mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-26 10:24:42 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 603b334b7f | |||
| 45071a8d8e | |||
| 95f264e04e | |||
| 77f2d2227a | |||
| 7854f5b9f7 | |||
| 5565a8ee10 | |||
| 9ad7067971 |
@@ -141,6 +141,7 @@ _Avoid_: Response envelope
|
||||
- Promise streaming methods return a lazy `AsyncIterable` directly rather than a Promise-wrapped stream object. Iteration opens the connection, `AbortSignal` cancels it, and ending iteration closes the underlying request; the Effect emitter analogously returns `Stream` directly.
|
||||
- Promise SSE connection establishment, declared HTTP failures, and infrastructure failures occur during `AsyncIterable` iteration, beginning with its first `next()` call, rather than during synchronous method construction.
|
||||
- Neither generated streaming runtime automatically reconnects after disconnection. Promise `AsyncIterable` and Effect `Stream` fail explicitly; live consumers refresh and resubscribe, while durable sequence-based resume remains explicit composition above the generated client.
|
||||
- Event definition durability is authoritative for published payloads. Durable definitions publish and decode only with commit metadata (`aggregateID`, `seq`, and `version`); live definitions forbid that metadata. Core's pre-commit payload without an assigned sequence is a separate internal type and never reaches subscribers or projectors.
|
||||
- Promise client construction is synchronous and network-free. It requires `baseUrl`, defaults to `globalThis.fetch`, accepts client-level headers, and merges them with per-call header overrides.
|
||||
- Effect client construction accepts an explicit `baseUrl` and obtains `HttpClient.HttpClient` from the Effect environment. It does not install fetch or duplicate per-call transport policy; callers transform/provide the client for headers, tracing, retries, recording, and tests, while fiber interruption owns cancellation.
|
||||
- Promise and Effect emitters each own their generated public type modules. The **SDK Contract IR**, not a physically shared generated type package, is the common source; this permits zero-Effect wire types and rich decoded Effect types to evolve independently.
|
||||
|
||||
@@ -3,12 +3,18 @@ import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { DebugBar } from "@/components/debug-bar"
|
||||
import { HelpButton } from "@/components/help-button"
|
||||
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { setNavigate } from "@/utils/notification-click"
|
||||
import { setV2Toast, ToastRegion } from "@/utils/toast"
|
||||
|
||||
export default function NewLayout(props: ParentProps) {
|
||||
const command = useCommand()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const notification = useNotification()
|
||||
const navigate = useNavigate()
|
||||
@@ -22,6 +28,20 @@ export default function NewLayout(props: ParentProps) {
|
||||
notification.session.markViewed(params.id)
|
||||
})
|
||||
|
||||
command.register("layout", () => [
|
||||
{
|
||||
id: "settings.open",
|
||||
title: language.t("command.settings.open"),
|
||||
category: language.t("command.category.settings"),
|
||||
keybind: "mod+comma",
|
||||
onSelect: () => {
|
||||
void import("@/components/settings-v2").then((x) => {
|
||||
dialog.show(() => <x.DialogSettings />)
|
||||
})
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const update: TitlebarUpdate = {
|
||||
version: () => {
|
||||
const state = platform.updater?.state()
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function NewSessionPage() {
|
||||
const serverSync = useServerSync()
|
||||
const comments = useComments()
|
||||
const route = useSessionKey()
|
||||
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
||||
const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>()
|
||||
|
||||
let inputRef: HTMLDivElement | undefined
|
||||
|
||||
|
||||
+67
-60
@@ -2,7 +2,14 @@ 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 type {
|
||||
Data,
|
||||
Definition,
|
||||
DurableDefinition,
|
||||
LiveDefinition,
|
||||
Payload,
|
||||
UncommittedPayload,
|
||||
} from "@opencode-ai/schema/event"
|
||||
import { and, asc, eq, gt } from "drizzle-orm"
|
||||
import { Database } from "./database/database"
|
||||
import { EventSequenceTable, EventTable } from "./event/sql"
|
||||
@@ -124,8 +131,25 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
)
|
||||
|
||||
function commitDurableEvent(
|
||||
definition: Definition,
|
||||
event: Payload,
|
||||
definition: DurableDefinition,
|
||||
event: UncommittedPayload<DurableDefinition>,
|
||||
input: undefined,
|
||||
commit?: (seq: number) => Effect.Effect<void>,
|
||||
): Effect.Effect<Payload<DurableDefinition>>
|
||||
function commitDurableEvent(
|
||||
definition: DurableDefinition,
|
||||
event: UncommittedPayload<DurableDefinition>,
|
||||
input: {
|
||||
readonly seq: number
|
||||
readonly aggregateID: string
|
||||
readonly ownerID?: string
|
||||
readonly strictOwner?: boolean
|
||||
},
|
||||
commit?: (seq: number) => Effect.Effect<void>,
|
||||
): Effect.Effect<Payload<DurableDefinition> | undefined>
|
||||
function commitDurableEvent(
|
||||
definition: DurableDefinition,
|
||||
event: UncommittedPayload<DurableDefinition>,
|
||||
input?: {
|
||||
readonly seq: number
|
||||
readonly aggregateID: string
|
||||
@@ -135,7 +159,7 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
commit?: (seq: number) => Effect.Effect<void>,
|
||||
) {
|
||||
return Effect.gen(function* () {
|
||||
const durable = definition?.durable
|
||||
const durable = definition.durable
|
||||
if (durable) {
|
||||
const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate]
|
||||
if (typeof aggregateID !== "string") {
|
||||
@@ -200,7 +224,7 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
}
|
||||
return
|
||||
return undefined
|
||||
}
|
||||
yield* Effect.die(
|
||||
new InvalidDurableEventError({
|
||||
@@ -210,7 +234,7 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
)
|
||||
}
|
||||
if (input && row?.ownerID && row.ownerID !== input.ownerID) {
|
||||
return
|
||||
return undefined
|
||||
}
|
||||
const seq = input?.seq ?? latest + 1
|
||||
if (input && seq !== latest + 1) {
|
||||
@@ -234,10 +258,10 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
message: `Event ${event.id} already exists at aggregate ${stored.aggregateID} sequence ${stored.seq}`,
|
||||
}),
|
||||
)
|
||||
const committed = {
|
||||
const committed: Payload<DurableDefinition> = {
|
||||
...event,
|
||||
durable: { aggregateID, seq, version: durable.version },
|
||||
} as Payload
|
||||
}
|
||||
for (const projector of list) {
|
||||
yield* projector(committed)
|
||||
}
|
||||
@@ -267,14 +291,14 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
])
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
return { aggregateID, seq }
|
||||
return committed
|
||||
}),
|
||||
{ behavior: "immediate" },
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
if (committed) {
|
||||
yield* Effect.forEach(
|
||||
pubsub.durable.get(committed.aggregateID) ?? [],
|
||||
pubsub.durable.get(committed.durable.aggregateID) ?? [],
|
||||
(wake) => PubSub.publish(wake, undefined),
|
||||
{ discard: true },
|
||||
)
|
||||
@@ -287,35 +311,6 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
})
|
||||
}
|
||||
|
||||
function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) {
|
||||
return Effect.gen(function* () {
|
||||
if (!definition?.durable && commit)
|
||||
return yield* Effect.die(
|
||||
new InvalidDurableEventError({
|
||||
type: event.type,
|
||||
message: "Local commit hooks require a durable event",
|
||||
}),
|
||||
)
|
||||
if (definition?.durable) {
|
||||
const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit)
|
||||
if (committed) {
|
||||
event = {
|
||||
...event,
|
||||
durable: {
|
||||
aggregateID: committed.aggregateID,
|
||||
seq: committed.seq,
|
||||
version: definition.durable.version,
|
||||
},
|
||||
}
|
||||
yield* notify(event as Payload, true)
|
||||
return event
|
||||
}
|
||||
}
|
||||
yield* notify(event as Payload, false)
|
||||
return event
|
||||
})
|
||||
}
|
||||
|
||||
const observe = (event: Payload, observer: (event: Payload) => Effect.Effect<void>) =>
|
||||
Effect.suspend(() => observer(event)).pipe(
|
||||
Effect.catchCauseIf(
|
||||
@@ -337,7 +332,12 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
})
|
||||
}
|
||||
|
||||
function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
|
||||
function publish<D extends Definition>(
|
||||
definition: D,
|
||||
data: Data<D>,
|
||||
options?: PublishOptions,
|
||||
): Effect.Effect<Payload<D>>
|
||||
function publish(definition: Definition, data: unknown, options?: PublishOptions): Effect.Effect<Payload> {
|
||||
return Effect.gen(function* () {
|
||||
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
|
||||
const location =
|
||||
@@ -345,17 +345,34 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
(serviceLocation
|
||||
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
|
||||
: undefined)
|
||||
return yield* publishEvent(
|
||||
definition,
|
||||
{
|
||||
if (definition.durable) {
|
||||
const event: UncommittedPayload<DurableDefinition> = {
|
||||
id: options?.id ?? ID.create(),
|
||||
...(options?.metadata ? { metadata: options.metadata } : {}),
|
||||
type: definition.type,
|
||||
...(location ? { location } : {}),
|
||||
data,
|
||||
} as Payload<D>,
|
||||
options?.commit,
|
||||
)
|
||||
}
|
||||
const committed = yield* commitDurableEvent(definition, event, undefined, options?.commit)
|
||||
yield* notify(committed, true)
|
||||
return committed
|
||||
}
|
||||
if (options?.commit)
|
||||
return yield* Effect.die(
|
||||
new InvalidDurableEventError({
|
||||
type: definition.type,
|
||||
message: "Local commit hooks require a durable event",
|
||||
}),
|
||||
)
|
||||
const event: Payload<LiveDefinition> = {
|
||||
id: options?.id ?? ID.create(),
|
||||
...(options?.metadata ? { metadata: options.metadata } : {}),
|
||||
type: definition.type,
|
||||
...(location ? { location } : {}),
|
||||
data,
|
||||
}
|
||||
yield* notify(event, false)
|
||||
return event
|
||||
})
|
||||
}
|
||||
|
||||
@@ -370,11 +387,11 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
|
||||
)
|
||||
} else {
|
||||
const payload = {
|
||||
const payload: UncommittedPayload<DurableDefinition> = {
|
||||
id: event.id,
|
||||
type: definition.type,
|
||||
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||
} as Payload
|
||||
}
|
||||
const committed = yield* commitDurableEvent(definition, payload, {
|
||||
seq: event.seq,
|
||||
aggregateID: event.aggregateID,
|
||||
@@ -382,17 +399,7 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
strictOwner: options?.strictOwner,
|
||||
})
|
||||
if (committed && options?.publish) {
|
||||
yield* notify(
|
||||
{
|
||||
...payload,
|
||||
durable: {
|
||||
aggregateID: committed.aggregateID,
|
||||
seq: committed.seq,
|
||||
version: definition.durable.version,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
yield* notify(committed, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -459,7 +466,7 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
|
||||
const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all)
|
||||
|
||||
const decodeSerializedEvent = (event: SerializedEvent) => {
|
||||
const decodeSerializedEvent = (event: SerializedEvent): Payload<DurableDefinition> => {
|
||||
const definition = Durable.get(event.type)
|
||||
if (!definition?.durable) {
|
||||
throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export * as PluginV2 from "./plugin"
|
||||
|
||||
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 type { Plugin as PluginRuntime } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Plugin } from "@opencode-ai/schema/plugin"
|
||||
import { AgentV2 } from "./agent"
|
||||
import { AISDK } from "./aisdk"
|
||||
import { Catalog } from "./catalog"
|
||||
@@ -15,13 +15,12 @@ import { Reference } from "./reference"
|
||||
import { SkillV2 } from "./skill"
|
||||
import { State } from "./state"
|
||||
|
||||
export const ID = PluginID
|
||||
export const ID = Plugin.ID
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Event = PluginEvent
|
||||
export const Event = Plugin.Event
|
||||
|
||||
export interface Interface {
|
||||
readonly add: (id: ID, effect: Plugin["effect"]) => Effect.Effect<void>
|
||||
readonly add: (id: ID, effect: PluginRuntime["effect"]) => Effect.Effect<void>
|
||||
readonly remove: (id: ID) => Effect.Effect<void>
|
||||
readonly wait: (id: ID) => Effect.Effect<void>
|
||||
}
|
||||
@@ -38,9 +37,9 @@ export const layer = Layer.effect(
|
||||
const loading = new Set<ID>()
|
||||
const waiters = new Map<ID, Set<Deferred.Deferred<void>>>()
|
||||
const failures = new Map<ID, Exit.Exit<void, never>>()
|
||||
let host: Parameters<Plugin["effect"]>[0]
|
||||
let host: Parameters<PluginRuntime["effect"]>[0]
|
||||
|
||||
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: Plugin["effect"]) {
|
||||
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginRuntime["effect"]) {
|
||||
if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`)
|
||||
|
||||
yield* locks.withLock(id)(
|
||||
|
||||
@@ -13,7 +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"
|
||||
import { Event } from "@opencode-ai/schema/project-directories"
|
||||
import { ProjectCopy } from "@opencode-ai/schema/project-copy"
|
||||
|
||||
export const StrategyID = ProjectCopy.StrategyID
|
||||
@@ -96,7 +96,7 @@ export interface Strategy {
|
||||
readonly list: (directory: AbsolutePath) => Effect.Effect<ListEntry[], Git.WorktreeError | DirectoryUnavailableError>
|
||||
}
|
||||
|
||||
export const Event = ProjectDirectoriesEvent
|
||||
export { Event }
|
||||
|
||||
export interface Interface {
|
||||
readonly register: (strategy: Strategy) => Effect.Effect<void, DuplicateStrategyError>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { Database } from "../database/database"
|
||||
import { LayerNode } from "../effect/layer-node"
|
||||
import { AbsolutePath, optionalOmitUndefined } from "../schema"
|
||||
import { AbsolutePath, optional } from "../schema"
|
||||
import { ProjectSchema } from "./schema"
|
||||
import { ProjectDirectoryTable } from "./sql"
|
||||
import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
@@ -39,7 +39,7 @@ export type ListInput = typeof ListInput.Type
|
||||
export const ListOutput = Schema.Array(
|
||||
Schema.Struct({
|
||||
directory: AbsolutePath,
|
||||
strategy: optionalOmitUndefined(Schema.String),
|
||||
strategy: optional(Schema.String),
|
||||
}),
|
||||
).annotate({ identifier: "Project.Directories" })
|
||||
export type ListOutput = typeof ListOutput.Type
|
||||
|
||||
@@ -2,7 +2,7 @@ export * as Pty from "./pty"
|
||||
|
||||
import type { Disp, Proc } from "#pty"
|
||||
import { Context, Effect, Layer, Schema, Types } from "effect"
|
||||
import { PtyEvent, PtyInfo, Pty } from "@opencode-ai/schema/pty"
|
||||
import { Pty } from "@opencode-ai/schema/pty"
|
||||
import { Config } from "./config"
|
||||
import { EventV2 } from "./event"
|
||||
import { Location } from "./location"
|
||||
@@ -35,8 +35,7 @@ type Active = {
|
||||
listeners: Disp[]
|
||||
}
|
||||
|
||||
export const Info = PtyInfo
|
||||
|
||||
export const Info = Pty.Info
|
||||
export type Info = Types.DeepMutable<typeof Info.Type>
|
||||
|
||||
export const CreateInput = Pty.CreateInput
|
||||
@@ -47,6 +46,8 @@ export const UpdateInput = Pty.UpdateInput
|
||||
|
||||
export type UpdateInput = Types.DeepMutable<typeof UpdateInput.Type>
|
||||
|
||||
export const Event = Pty.Event
|
||||
|
||||
export type AttachInput = {
|
||||
// Absolute output cursor to replay from. -1 tails from the current end; omitted replays the full retained buffer.
|
||||
readonly cursor?: number
|
||||
@@ -75,8 +76,6 @@ export class ExitedError extends Schema.TaggedErrorClass<ExitedError>()("Pty.Exi
|
||||
ptyID: PtyID,
|
||||
}) {}
|
||||
|
||||
export const Event = PtyEvent
|
||||
|
||||
export interface Interface {
|
||||
readonly list: () => Effect.Effect<Info[]>
|
||||
readonly get: (id: PtyID) => Effect.Effect<Info, NotFoundError>
|
||||
|
||||
@@ -3,21 +3,13 @@ import {
|
||||
AbsolutePath,
|
||||
DateTimeUtcFromMillis,
|
||||
NonNegativeInt,
|
||||
optionalOmitUndefined,
|
||||
optional,
|
||||
PositiveInt,
|
||||
RelativePath,
|
||||
withStatics,
|
||||
statics,
|
||||
} from "@opencode-ai/schema/schema"
|
||||
|
||||
export {
|
||||
AbsolutePath,
|
||||
DateTimeUtcFromMillis,
|
||||
NonNegativeInt,
|
||||
optionalOmitUndefined,
|
||||
PositiveInt,
|
||||
RelativePath,
|
||||
withStatics,
|
||||
}
|
||||
export { AbsolutePath, DateTimeUtcFromMillis, NonNegativeInt, optional, PositiveInt, RelativePath, statics }
|
||||
|
||||
/**
|
||||
* Strip `readonly` from a nested type. Stand-in for `effect`'s `Types.DeepMutable`
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ContextSnapshotDecodeError } from "./error"
|
||||
import { SessionEvent } from "./event"
|
||||
import { SessionHistory } from "./history"
|
||||
import { SessionInput } from "./input"
|
||||
import { SessionMessageID } from "./message-id"
|
||||
import { SessionMessage } from "./message"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { SessionContextEpochTable } from "./sql"
|
||||
|
||||
@@ -71,7 +71,7 @@ const prepareOnce = Effect.fnUntraced(function* (
|
||||
|
||||
yield* events.publish(
|
||||
SessionEvent.ContextUpdated,
|
||||
{ sessionID, messageID: SessionMessageID.ID.create(), timestamp: yield* DateTime.now, text: result.text },
|
||||
{ sessionID, messageID: SessionMessage.ID.create(), timestamp: yield* DateTime.now, text: result.text },
|
||||
{ commit: () => advance(db, sessionID, result.snapshot).pipe(Effect.orDie) },
|
||||
)
|
||||
return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AbsolutePath, RelativePath } from "../schema"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { SessionTable } from "./sql"
|
||||
import { SessionMessageID } from "./message-id"
|
||||
import { SessionMessage } from "./message"
|
||||
import { Snapshot } from "../snapshot"
|
||||
|
||||
export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.Info {
|
||||
@@ -40,7 +40,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
|
||||
workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined,
|
||||
}),
|
||||
subpath: row.path ? RelativePath.make(row.path) : undefined,
|
||||
revert: row.revert ? { ...row.revert, messageID: SessionMessageID.ID.make(row.revert.messageID) } : undefined,
|
||||
revert: row.revert ? { ...row.revert, messageID: SessionMessage.ID.make(row.revert.messageID) } : undefined,
|
||||
time: {
|
||||
created: DateTime.makeUnsafe(row.time_created),
|
||||
updated: DateTime.makeUnsafe(row.time_updated),
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * as SessionMessageID from "./message-id"
|
||||
export { ID } from "@opencode-ai/schema/session-message-id"
|
||||
@@ -15,7 +15,6 @@ import { WorkspaceV2 } from "../workspace"
|
||||
import { SessionContextEpoch } from "./context-epoch"
|
||||
import { MessageTable, PartTable, SessionInputTable, SessionMessageTable, SessionTable } from "./sql"
|
||||
import type { DeepMutable } from "../schema"
|
||||
import { SessionMessageID } from "./message-id"
|
||||
|
||||
type DatabaseService = Database.Interface["db"]
|
||||
|
||||
@@ -67,7 +66,7 @@ function sessionRow(info: SessionV1.SessionInfo): typeof SessionTable.$inferInse
|
||||
tokens_reasoning: (info.tokens ?? { reasoning: 0 }).reasoning,
|
||||
tokens_cache_read: (info.tokens ?? { cache: { read: 0 } }).cache.read,
|
||||
tokens_cache_write: (info.tokens ?? { cache: { write: 0 } }).cache.write,
|
||||
revert: info.revert ? { ...info.revert, messageID: SessionMessageID.ID.make(info.revert.messageID) } : null,
|
||||
revert: info.revert ? { ...info.revert, messageID: SessionMessage.ID.make(info.revert.messageID) } : null,
|
||||
permission: info.permission ? [...info.permission] : undefined,
|
||||
time_created: info.time.created,
|
||||
time_updated: info.time.updated,
|
||||
|
||||
@@ -2,15 +2,14 @@ export * as SessionTodo from "./todo"
|
||||
|
||||
import { asc, eq } from "drizzle-orm"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { SessionTodo, SessionTodoInfo } from "@opencode-ai/schema/session-todo"
|
||||
import { SessionTodo } 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 = SessionTodoInfo
|
||||
export const Info = SessionTodo.Info
|
||||
export type Info = typeof Info.Type
|
||||
|
||||
export const Event = SessionTodo.Event
|
||||
|
||||
export interface Interface {
|
||||
|
||||
@@ -116,7 +116,7 @@ describe("EventV2", () => {
|
||||
const event = yield* events.publish(VersionedMessage, { id: "one", text: "hello" })
|
||||
|
||||
expect(event.type).toBe("test.versioned")
|
||||
expect(event.durable?.version).toBe(2)
|
||||
expect(event.durable.version).toBe(2)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -138,6 +138,50 @@ describe("EventV2", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves same-type projector routing across durable versions", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const historical = EventV2.define({
|
||||
type: "test.projector-version",
|
||||
durable: { version: 1, aggregate: "id" },
|
||||
schema: { id: Schema.String },
|
||||
})
|
||||
const current = EventV2.define({
|
||||
type: "test.projector-version",
|
||||
durable: { version: 2, aggregate: "id" },
|
||||
schema: { id: Schema.String },
|
||||
})
|
||||
const received = new Array<EventV2.Payload>()
|
||||
yield* events.project(historical, (event) => Effect.sync(() => received.push(event)))
|
||||
|
||||
const published = yield* events.publish(current, { id: "aggregate" })
|
||||
|
||||
expect(received).toEqual([published])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves same-type subscription routing across durable versions", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const historical = EventV2.define({
|
||||
type: "test.subscription-version",
|
||||
durable: { version: 1, aggregate: "id" },
|
||||
schema: { id: Schema.String },
|
||||
})
|
||||
const current = EventV2.define({
|
||||
type: "test.subscription-version",
|
||||
durable: { version: 2, aggregate: "id" },
|
||||
schema: { id: Schema.String },
|
||||
})
|
||||
const fiber = yield* events.subscribe(historical).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||
yield* Effect.yieldNow
|
||||
|
||||
const published = yield* events.publish(current, { id: "aggregate" })
|
||||
|
||||
expect(Array.from(yield* Fiber.join(fiber))).toEqual([published])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("publishes to typed and wildcard subscriptions", () =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
@@ -764,7 +808,7 @@ describe("EventV2", () => {
|
||||
const replayed = {
|
||||
id: published.id,
|
||||
type: EventV2.versionedType(DurableMessage.type, 1),
|
||||
seq: published.durable!.seq,
|
||||
seq: published.durable.seq,
|
||||
aggregateID,
|
||||
data: published.data,
|
||||
}
|
||||
|
||||
@@ -17,7 +17,14 @@ const capture = () => {
|
||||
const events = EventV2.Service.of({
|
||||
publish: (definition, data) =>
|
||||
Effect.sync(() => {
|
||||
const event = { id: EventV2.ID.create(), type: definition.type, data } as EventV2.Payload<typeof definition>
|
||||
const event = {
|
||||
id: EventV2.ID.create(),
|
||||
type: definition.type,
|
||||
...(definition.durable
|
||||
? { durable: { aggregateID: sessionID, seq: published.length, version: definition.durable.version } }
|
||||
: {}),
|
||||
data,
|
||||
} as EventV2.Payload<typeof definition>
|
||||
published.push({
|
||||
type: definition.durable
|
||||
? EventV2.versionedType(definition.type, definition.durable.version)
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Model } from "@opencode-ai/schema/model"
|
||||
import { AgentAttachment, FileAttachment, Prompt, Source } from "@opencode-ai/schema/prompt"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import { Project } from "@opencode-ai/schema/project"
|
||||
import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
|
||||
import { PermissionV1 } from "@opencode-ai/schema/permission-v1"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { SessionInput } from "@opencode-ai/schema/session-input"
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
@@ -20,10 +22,14 @@ import { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||
import { Integration } from "@opencode-ai/schema/integration"
|
||||
import { LLM } from "@opencode-ai/schema/llm"
|
||||
import { Permission } from "@opencode-ai/schema/permission"
|
||||
import { Plugin } from "@opencode-ai/schema/plugin"
|
||||
import { Pty } from "@opencode-ai/schema/pty"
|
||||
import { Reference } from "@opencode-ai/schema/reference"
|
||||
import { SessionTodo } from "@opencode-ai/schema/session-todo"
|
||||
import { Skill } from "@opencode-ai/schema/skill"
|
||||
import { AbsolutePath, DateTimeUtcFromMillis } from "@opencode-ai/schema/schema"
|
||||
import { AbsolutePath, DateTimeUtcFromMillis, optional, statics } from "@opencode-ai/schema/schema"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
|
||||
test("Core reuses the canonical shared schemas", async () => {
|
||||
const [
|
||||
@@ -35,13 +41,18 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
coreLocation,
|
||||
coreLLM,
|
||||
corePermission,
|
||||
corePermissionV1,
|
||||
coreProjectCopy,
|
||||
corePty,
|
||||
coreProject,
|
||||
coreReference,
|
||||
coreSessionInput,
|
||||
coreSessionMessage,
|
||||
coreSessionTodo,
|
||||
corePrompt,
|
||||
coreSkill,
|
||||
coreV2Schema,
|
||||
coreSchema,
|
||||
coreWorkspace,
|
||||
] = await Promise.all([
|
||||
import("@opencode-ai/core/command"),
|
||||
@@ -52,13 +63,18 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
import("@opencode-ai/core/location"),
|
||||
import("@opencode-ai/llm"),
|
||||
import("@opencode-ai/core/permission"),
|
||||
import("@opencode-ai/core/v1/permission"),
|
||||
import("@opencode-ai/core/project/copy"),
|
||||
import("@opencode-ai/core/pty"),
|
||||
import("@opencode-ai/core/project/schema"),
|
||||
import("@opencode-ai/core/reference"),
|
||||
import("@opencode-ai/core/session/input"),
|
||||
import("@opencode-ai/core/session/message"),
|
||||
import("@opencode-ai/core/session/todo"),
|
||||
import("@opencode-ai/core/session/prompt"),
|
||||
import("@opencode-ai/core/skill"),
|
||||
import("@opencode-ai/core/v2-schema"),
|
||||
import("@opencode-ai/core/schema"),
|
||||
import("@opencode-ai/core/workspace"),
|
||||
])
|
||||
|
||||
@@ -111,6 +127,12 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
[corePermission.Effect, Permission.Effect],
|
||||
[corePermission.Rule, Permission.Rule],
|
||||
[corePermission.Ruleset, Permission.Ruleset],
|
||||
[corePermissionV1.Event, PermissionV1.Event],
|
||||
[coreProjectCopy.Event, ProjectDirectories.Event],
|
||||
[PluginV2.ID, Plugin.ID],
|
||||
[PluginV2.Event, Plugin.Event],
|
||||
[corePty.Info, Pty.Info],
|
||||
[corePty.Event, Pty.Event],
|
||||
[coreProject.ID, Project.ID],
|
||||
[coreReference.LocalSource, Reference.LocalSource],
|
||||
[coreReference.GitSource, Reference.GitSource],
|
||||
@@ -140,6 +162,8 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
[coreSessionMessage.Assistant, SessionMessage.Assistant],
|
||||
[coreSessionMessage.Compaction, SessionMessage.Compaction],
|
||||
[coreSessionMessage.Message, SessionMessage.Message],
|
||||
[coreSessionTodo.Info, SessionTodo.Info],
|
||||
[coreSessionTodo.Event, SessionTodo.Event],
|
||||
[corePrompt.Source, Source],
|
||||
[corePrompt.FileAttachment, FileAttachment],
|
||||
[corePrompt.AgentAttachment, AgentAttachment],
|
||||
@@ -150,6 +174,8 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
[coreSkill.Source, Skill.Source],
|
||||
[coreSkill.Info, Skill.Info],
|
||||
[coreV2Schema.DateTimeUtcFromMillis, DateTimeUtcFromMillis],
|
||||
[coreSchema.optional, optional],
|
||||
[coreSchema.statics, statics],
|
||||
[coreWorkspace.ID, Workspace.ID],
|
||||
]
|
||||
for (const [core, shared] of schemas) expect(core).toBe(shared)
|
||||
|
||||
@@ -6,9 +6,8 @@ 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 { PermissionV1Event } from "@opencode-ai/schema/permission-v1"
|
||||
|
||||
export const Event = PermissionV1Event
|
||||
export const Event = PermissionV1.Event
|
||||
|
||||
export interface Interface {
|
||||
readonly ask: (input: PermissionV1.AskInput) => Effect.Effect<void, PermissionV1.Error>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { Auth } from "@/auth"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
import { optional } from "@opencode-ai/core/schema"
|
||||
import { Plugin } from "../plugin"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect"
|
||||
@@ -18,14 +18,14 @@ const TextPrompt = Schema.Struct({
|
||||
type: Schema.Literal("text"),
|
||||
key: Schema.String,
|
||||
message: Schema.String,
|
||||
placeholder: optionalOmitUndefined(Schema.String),
|
||||
when: optionalOmitUndefined(When),
|
||||
placeholder: optional(Schema.String),
|
||||
when: optional(When),
|
||||
})
|
||||
|
||||
const SelectOption = Schema.Struct({
|
||||
label: Schema.String,
|
||||
value: Schema.String,
|
||||
hint: optionalOmitUndefined(Schema.String),
|
||||
hint: optional(Schema.String),
|
||||
})
|
||||
|
||||
const SelectPrompt = Schema.Struct({
|
||||
@@ -33,7 +33,7 @@ const SelectPrompt = Schema.Struct({
|
||||
key: Schema.String,
|
||||
message: Schema.String,
|
||||
options: Schema.Array(SelectOption),
|
||||
when: optionalOmitUndefined(When),
|
||||
when: optional(When),
|
||||
})
|
||||
|
||||
const Prompt = Schema.Union([TextPrompt, SelectPrompt])
|
||||
@@ -41,7 +41,7 @@ const Prompt = Schema.Union([TextPrompt, SelectPrompt])
|
||||
export class Method extends Schema.Class<Method>("ProviderAuthMethod")({
|
||||
type: Schema.Literals(["oauth", "api"]),
|
||||
label: Schema.String,
|
||||
prompts: optionalOmitUndefined(Schema.Array(Prompt)),
|
||||
prompts: optional(Schema.Array(Prompt)),
|
||||
}) {}
|
||||
|
||||
export const Methods = Schema.Record(Schema.String, Schema.Array(Method))
|
||||
|
||||
@@ -24,7 +24,7 @@ import { InstanceState } from "@/effect/instance-state"
|
||||
import { EffectPromise } from "@/effect/promise"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
import { optional } from "@opencode-ai/core/schema"
|
||||
import { ProviderTransform } from "./transform"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
@@ -999,8 +999,8 @@ const ProviderCost = Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
cache: ProviderCacheCost,
|
||||
tiers: optionalOmitUndefined(Schema.Array(ProviderCostTier)),
|
||||
experimentalOver200K: optionalOmitUndefined(
|
||||
tiers: optional(Schema.Array(ProviderCostTier)),
|
||||
experimentalOver200K: optional(
|
||||
Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
@@ -1011,7 +1011,7 @@ const ProviderCost = Schema.Struct({
|
||||
|
||||
const ProviderLimit = Schema.Struct({
|
||||
context: Schema.Finite,
|
||||
input: optionalOmitUndefined(Schema.Finite),
|
||||
input: optional(Schema.Finite),
|
||||
output: Schema.Finite,
|
||||
})
|
||||
|
||||
@@ -1020,7 +1020,7 @@ export const Model = Schema.Struct({
|
||||
providerID: ProviderV2.ID,
|
||||
api: ProviderApiInfo,
|
||||
name: Schema.String,
|
||||
family: optionalOmitUndefined(Schema.String),
|
||||
family: optional(Schema.String),
|
||||
capabilities: ProviderCapabilities,
|
||||
cost: ProviderCost,
|
||||
limit: ProviderLimit,
|
||||
@@ -1028,7 +1028,7 @@ export const Model = Schema.Struct({
|
||||
options: Schema.Record(Schema.String, Schema.Any),
|
||||
headers: Schema.Record(Schema.String, Schema.String),
|
||||
release_date: Schema.String,
|
||||
variants: optionalOmitUndefined(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))),
|
||||
variants: optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))),
|
||||
}).annotate({ identifier: "Model" })
|
||||
export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
|
||||
|
||||
@@ -1037,7 +1037,7 @@ export const Info = Schema.Struct({
|
||||
name: Schema.String,
|
||||
source: Schema.Literals(["env", "config", "custom", "api"]),
|
||||
env: Schema.Array(Schema.String),
|
||||
key: optionalOmitUndefined(Schema.String),
|
||||
key: optional(Schema.String),
|
||||
options: Schema.Record(Schema.String, Schema.Any),
|
||||
models: Schema.Record(Schema.String, Model),
|
||||
}).annotate({ identifier: "Provider" })
|
||||
|
||||
@@ -2,14 +2,14 @@ import { Schema } from "effect"
|
||||
|
||||
import { Identifier } from "@/id/id"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
import { statics } from "@opencode-ai/core/schema"
|
||||
|
||||
export const SessionID = SessionV2.ID
|
||||
export type SessionID = Schema.Schema.Type<typeof SessionID>
|
||||
|
||||
export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe(
|
||||
Schema.brand("MessageID"),
|
||||
withStatics((s) => ({
|
||||
statics((s) => ({
|
||||
ascending: (id?: string) => s.make(Identifier.ascending("message", id)),
|
||||
})),
|
||||
)
|
||||
@@ -18,7 +18,7 @@ export type MessageID = Schema.Schema.Type<typeof MessageID>
|
||||
|
||||
export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe(
|
||||
Schema.brand("PartID"),
|
||||
withStatics((s) => ({
|
||||
statics((s) => ({
|
||||
ascending: (id?: string) => s.make(Identifier.ascending("part", id)),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -39,11 +39,11 @@ import { SessionID, MessageID, PartID } from "./schema"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Effect, Layer, Option, Context, Schema, Types } from "effect"
|
||||
import { NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
import { NonNegativeInt, optional } from "@opencode-ai/core/schema"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { SessionMessageID } from "@opencode-ai/schema/session-message-id"
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
|
||||
const runtime = makeRuntime(Database.Service, Database.defaultLayer)
|
||||
|
||||
@@ -146,7 +146,7 @@ export function toRow(info: Info) {
|
||||
tokens_cache_write: (info.tokens ?? EmptyTokens).cache.write,
|
||||
revert: info.revert
|
||||
? {
|
||||
messageID: SessionMessageID.ID.make(info.revert.messageID),
|
||||
messageID: SessionMessage.ID.make(info.revert.messageID),
|
||||
partID: info.revert.partID,
|
||||
snapshot: info.revert.snapshot,
|
||||
diff: info.revert.diff,
|
||||
@@ -178,7 +178,7 @@ const Summary = Schema.Struct({
|
||||
additions: Schema.Finite,
|
||||
deletions: Schema.Finite,
|
||||
files: Schema.Finite,
|
||||
diffs: optionalOmitUndefined(Schema.Array(Snapshot.FileDiff)),
|
||||
diffs: optional(Schema.Array(Snapshot.FileDiff)),
|
||||
})
|
||||
|
||||
const Tokens = Schema.Struct({
|
||||
@@ -204,21 +204,21 @@ export const ArchivedTimestamp = Schema.Finite
|
||||
const Time = Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
updated: NonNegativeInt,
|
||||
compacting: optionalOmitUndefined(NonNegativeInt),
|
||||
archived: optionalOmitUndefined(ArchivedTimestamp),
|
||||
compacting: optional(NonNegativeInt),
|
||||
archived: optional(ArchivedTimestamp),
|
||||
})
|
||||
|
||||
const Revert = Schema.Struct({
|
||||
messageID: MessageID,
|
||||
partID: optionalOmitUndefined(PartID),
|
||||
snapshot: optionalOmitUndefined(Schema.String),
|
||||
diff: optionalOmitUndefined(Schema.String),
|
||||
partID: optional(PartID),
|
||||
snapshot: optional(Schema.String),
|
||||
diff: optional(Schema.String),
|
||||
})
|
||||
|
||||
const Model = Schema.Struct({
|
||||
id: ModelV2.ID,
|
||||
providerID: ProviderV2.ID,
|
||||
variant: optionalOmitUndefined(Schema.String),
|
||||
variant: optional(Schema.String),
|
||||
})
|
||||
|
||||
export const Metadata = Schema.Record(Schema.String, Schema.Any)
|
||||
@@ -227,28 +227,28 @@ export const Info = Schema.Struct({
|
||||
id: SessionID,
|
||||
slug: Schema.String,
|
||||
projectID: ProjectV2.ID,
|
||||
workspaceID: optionalOmitUndefined(WorkspaceV2.ID),
|
||||
workspaceID: optional(WorkspaceV2.ID),
|
||||
directory: Schema.String,
|
||||
path: optionalOmitUndefined(Schema.String),
|
||||
parentID: optionalOmitUndefined(SessionID),
|
||||
summary: optionalOmitUndefined(Summary),
|
||||
cost: optionalOmitUndefined(Schema.Finite),
|
||||
tokens: optionalOmitUndefined(Tokens),
|
||||
share: optionalOmitUndefined(Share),
|
||||
path: optional(Schema.String),
|
||||
parentID: optional(SessionID),
|
||||
summary: optional(Summary),
|
||||
cost: optional(Schema.Finite),
|
||||
tokens: optional(Tokens),
|
||||
share: optional(Share),
|
||||
title: Schema.String,
|
||||
agent: optionalOmitUndefined(Schema.String),
|
||||
model: optionalOmitUndefined(Model),
|
||||
agent: optional(Schema.String),
|
||||
model: optional(Model),
|
||||
version: Schema.String,
|
||||
metadata: optionalOmitUndefined(Metadata),
|
||||
metadata: optional(Metadata),
|
||||
time: Time,
|
||||
permission: optionalOmitUndefined(PermissionV1.Ruleset),
|
||||
revert: optionalOmitUndefined(Revert),
|
||||
permission: optional(PermissionV1.Ruleset),
|
||||
revert: optional(Revert),
|
||||
}).annotate({ identifier: "Session" })
|
||||
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||
|
||||
export const ProjectInfo = Schema.Struct({
|
||||
id: ProjectV2.ID,
|
||||
name: optionalOmitUndefined(Schema.String),
|
||||
name: optional(Schema.String),
|
||||
worktree: Schema.String,
|
||||
}).annotate({ identifier: "ProjectSummary" })
|
||||
export type ProjectInfo = Types.DeepMutable<Schema.Schema.Type<typeof ProjectInfo>>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { Identifier } from "@/id/id"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
import { statics } from "@opencode-ai/core/schema"
|
||||
|
||||
export const EventID = Schema.String.check(Schema.isStartsWith("evt")).pipe(
|
||||
Schema.brand("EventID"),
|
||||
withStatics((s) => ({
|
||||
statics((s) => ({
|
||||
ascending: (id?: string) => s.make(Identifier.ascending("event", id)),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { Identifier } from "@/id/id"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
import { statics } from "@opencode-ai/core/schema"
|
||||
|
||||
const toolIdSchema = Schema.String.check(Schema.isStartsWith("tool")).pipe(Schema.brand("ToolID"))
|
||||
|
||||
export type ToolID = typeof toolIdSchema.Type
|
||||
|
||||
export const ToolID = toolIdSchema.pipe(
|
||||
withStatics((schema: typeof toolIdSchema) => ({
|
||||
statics((schema: typeof toolIdSchema) => ({
|
||||
ascending: (id?: string) => schema.make(Identifier.ascending("tool", id)),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -99,6 +99,17 @@ describe("PublicApi OpenAPI v2 errors", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("documents durable metadata only on durable events", () => {
|
||||
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
|
||||
const durable = spec.components.schemas.V2EventSessionCreated
|
||||
const live = spec.components.schemas.V2EventSessionNextTextDelta
|
||||
|
||||
expect(durable?.required).toContain("durable")
|
||||
expect(durable?.properties?.durable).toBeDefined()
|
||||
expect(live?.required).not.toContain("durable")
|
||||
expect(Reflect.get(live?.properties?.durable ?? {}, "not")).toEqual({})
|
||||
})
|
||||
|
||||
test("preserves /api auth responses", () => {
|
||||
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ function request(route: string, directory: string, init: RequestInit = {}) {
|
||||
const Event = Schema.Struct({
|
||||
id: EventV2.ID,
|
||||
type: Schema.String,
|
||||
durable: Schema.optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Int, version: Schema.Int })),
|
||||
location: Schema.optional(Location.Ref),
|
||||
data: Schema.Unknown,
|
||||
})
|
||||
@@ -81,12 +82,15 @@ describe("v2 location HttpApi", () => {
|
||||
const reader = response.body!.getReader()
|
||||
const connected = await readEvent(reader)
|
||||
expect(connected.type).toBe("server.connected")
|
||||
expect(connected).not.toHaveProperty("durable")
|
||||
expect(connected.location).toBeUndefined()
|
||||
|
||||
const created = await request("/session", publisher.path, { method: "POST" })
|
||||
expect(created.status).toBe(200)
|
||||
const session = (await created.json()) as { id: string }
|
||||
expect(await readEventType(reader, "session.created")).toMatchObject({
|
||||
type: "session.created",
|
||||
durable: { aggregateID: session.id, seq: 0, version: 1 },
|
||||
location: { directory: publisher.path },
|
||||
data: { sessionID: expect.any(String) },
|
||||
})
|
||||
|
||||
@@ -9,6 +9,12 @@ import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
|
||||
function durable(sessionID: SessionID, seq?: number): { aggregateID: SessionID; seq: number; version: 1 }
|
||||
function durable(sessionID: SessionID, seq: number, version: 2): { aggregateID: SessionID; seq: number; version: 2 }
|
||||
function durable(sessionID: SessionID, seq = 0, version: 1 | 2 = 1) {
|
||||
return { aggregateID: sessionID, seq, version }
|
||||
}
|
||||
|
||||
test.skip("step snapshots carry over to assistant messages", () => {
|
||||
const state: SessionMessageUpdater.MemoryState = { messages: [] }
|
||||
const sessionID = SessionID.make("session")
|
||||
@@ -17,6 +23,7 @@ test.skip("step snapshots carry over to assistant messages", () => {
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
durable: durable(sessionID),
|
||||
type: "session.next.step.started",
|
||||
data: {
|
||||
sessionID,
|
||||
@@ -38,6 +45,7 @@ test.skip("step snapshots carry over to assistant messages", () => {
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
durable: durable(sessionID, 1, 2),
|
||||
type: "session.next.step.ended",
|
||||
data: {
|
||||
sessionID,
|
||||
@@ -70,6 +78,7 @@ test.skip("text ended populates assistant text content", () => {
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
durable: durable(sessionID),
|
||||
type: "session.next.step.started",
|
||||
data: {
|
||||
sessionID,
|
||||
@@ -88,6 +97,7 @@ test.skip("text ended populates assistant text content", () => {
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
durable: durable(sessionID, 1),
|
||||
type: "session.next.text.started",
|
||||
data: {
|
||||
sessionID,
|
||||
@@ -101,6 +111,7 @@ test.skip("text ended populates assistant text content", () => {
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
durable: durable(sessionID, 2),
|
||||
type: "session.next.text.ended",
|
||||
data: {
|
||||
sessionID,
|
||||
@@ -126,6 +137,7 @@ test.skip("tool completion stores completed timestamp", () => {
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
durable: durable(sessionID),
|
||||
type: "session.next.step.started",
|
||||
data: {
|
||||
sessionID,
|
||||
@@ -144,6 +156,7 @@ test.skip("tool completion stores completed timestamp", () => {
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
durable: durable(sessionID, 1),
|
||||
type: "session.next.tool.input.started",
|
||||
data: {
|
||||
sessionID,
|
||||
@@ -158,6 +171,7 @@ test.skip("tool completion stores completed timestamp", () => {
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
durable: durable(sessionID, 2),
|
||||
type: "session.next.tool.called",
|
||||
data: {
|
||||
sessionID,
|
||||
@@ -174,6 +188,7 @@ test.skip("tool completion stores completed timestamp", () => {
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
durable: durable(sessionID, 3),
|
||||
type: "session.next.tool.success",
|
||||
data: {
|
||||
sessionID,
|
||||
@@ -204,6 +219,7 @@ test("compaction events reduce to compaction message only when completed", () =>
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id,
|
||||
durable: durable(sessionID),
|
||||
type: "session.next.compaction.started",
|
||||
data: {
|
||||
sessionID,
|
||||
@@ -245,6 +261,7 @@ test("compaction events reduce to compaction message only when completed", () =>
|
||||
Effect.runSync(
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: EventV2.ID.create(),
|
||||
durable: durable(sessionID, 3),
|
||||
type: "session.next.compaction.ended",
|
||||
data: {
|
||||
sessionID,
|
||||
|
||||
@@ -8,24 +8,34 @@ import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
const fields = {
|
||||
id: Event.ID,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
durable: Schema.optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Int, version: Schema.Int })),
|
||||
location: Schema.optional(Location.Ref),
|
||||
}
|
||||
|
||||
const DurableEnvelope = Schema.Struct({ aggregateID: Schema.String, seq: Schema.Int, version: Schema.Int })
|
||||
|
||||
const schema = (definitions: ReadonlyArray<Definition>) =>
|
||||
Schema.Union([
|
||||
...definitions.map((definition) =>
|
||||
Schema.Struct({
|
||||
...fields,
|
||||
type: Schema.Literal(definition.type),
|
||||
data: definition.data,
|
||||
}).annotate({ identifier: `V2Event.${definition.type}` }),
|
||||
definition.durable
|
||||
? Schema.Struct({
|
||||
...fields,
|
||||
durable: DurableEnvelope,
|
||||
type: Schema.Literal(definition.type),
|
||||
data: definition.data,
|
||||
}).annotate({ identifier: `V2Event.${definition.type}` })
|
||||
: Schema.Struct({
|
||||
...fields,
|
||||
durable: Schema.optional(Schema.Never),
|
||||
type: Schema.Literal(definition.type),
|
||||
data: definition.data,
|
||||
}).annotate({ identifier: `V2Event.${definition.type}` }),
|
||||
),
|
||||
...(definitions.some((definition) => definition.type === "server.connected")
|
||||
? []
|
||||
: [
|
||||
Schema.Struct({
|
||||
...fields,
|
||||
durable: Schema.optional(Schema.Never),
|
||||
type: Schema.Literal("server.connected"),
|
||||
data: Schema.Struct({}),
|
||||
}).annotate({ identifier: "V2Event.server.connected" }),
|
||||
@@ -56,4 +66,5 @@ export const makeEventGroup = (definitions: ReadonlyArray<Definition>) => make(d
|
||||
|
||||
const event = make(EventManifest.ServerDefinitions)
|
||||
export const EventGroup = event.group
|
||||
export type Event = typeof event.schema.Type
|
||||
export const EventSchema = event.schema
|
||||
export type Event = typeof EventSchema.Type
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SessionInput } from "@opencode-ai/schema/session-input"
|
||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { Project } from "@opencode-ai/schema/project"
|
||||
import { AbsolutePath, PositiveInt, RelativePath, withStatics } from "@opencode-ai/schema/schema"
|
||||
import { AbsolutePath, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema"
|
||||
import { Workspace } from "@opencode-ai/schema/workspace"
|
||||
import { Context, Encoding, Result, Schema, Struct } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
@@ -62,7 +62,7 @@ const decodeSessionsCursor = Schema.decodeUnknownEffect(SessionsCursorJson)
|
||||
|
||||
export const SessionsCursor = Schema.String.pipe(
|
||||
Schema.brand("SessionsCursor"),
|
||||
withStatics((schema) => {
|
||||
statics((schema) => {
|
||||
const make = schema.make.bind(schema)
|
||||
return {
|
||||
make: (input: typeof SessionsCursorInput.Type) => make(Encoding.encodeBase64Url(encodeSessionsCursor(input))),
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { Schema } from "effect"
|
||||
import { EventSchema } from "../src/groups/event"
|
||||
|
||||
describe("EventSchema", () => {
|
||||
test("requires durable metadata on durable events", () => {
|
||||
expect(
|
||||
Schema.is(EventSchema)({
|
||||
id: Event.ID.create(),
|
||||
type: "session.created",
|
||||
data: { sessionID: "session" },
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects durable metadata on live events", () => {
|
||||
expect(
|
||||
Schema.is(EventSchema)({
|
||||
id: Event.ID.create(),
|
||||
type: "server.connected",
|
||||
durable: { aggregateID: "aggregate", seq: 0, version: 1 },
|
||||
data: {},
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
# Schema Package Guide
|
||||
|
||||
`@opencode-ai/schema` owns browser-safe wire and storage contracts shared by protocol, server, core, and generated SDKs. Keep runtime behavior, service layers, side effects, and host-local implementation details in the domain package that owns them.
|
||||
|
||||
## Package Boundary
|
||||
|
||||
- Preserve the dependency direction: `@opencode-ai/schema <- @opencode-ai/protocol <- @opencode-ai/server`.
|
||||
- Schema values should be serializable contract definitions, not service implementations or runtime registries.
|
||||
- A domain may keep a minimal public wire contract here when SDK generation needs it, but do not move the broader runtime model into Schema just because an event is public. `plugin.added` is the current example: Schema may own the minimum browser-safe event payload, while plugin runtime behavior stays outside Schema.
|
||||
- The root barrel exports canonical current domain contracts. Specialized event modules, manifests, infrastructure modules, and V1 contracts use direct entrypoints instead of becoming first-class root exports.
|
||||
|
||||
## Current Versus V1
|
||||
|
||||
- Current contracts are unversioned: use names like `Session`, `Permission`, `Question`, and identifiers like `Permission.Request`.
|
||||
- Legacy contracts retained for active compatibility, persistence, or migration are explicitly `V1`: use names like `SessionV1`, `PermissionV1`, and identifiers like `PermissionV1.Request`.
|
||||
- Do not preserve `V2` as the permanent name for the replacement architecture. Remove `V2` from current namespaces, brands, and identifiers as the contracts are normalized.
|
||||
- Retained V1 contracts should live under a dedicated `src/v1/` subtree once the V1 isolation PR runs. New/current code must not depend on that subtree.
|
||||
- V1 coexistence is temporary. Keep compatibility entrypoints only where migration requires them, and delete the V1 subtree when the legacy runtime is retired.
|
||||
- `@opencode-ai/protocol` and `@opencode-ai/sdk-next` are current `/api/...` surfaces.
|
||||
|
||||
## Events
|
||||
|
||||
- Classify event definitions by protocol role before adding them to a public manifest: `current`, `shared transitional`, or `V1-only`.
|
||||
- Being emitted by V1 is not enough to include an event in Protocol or SDK Next.
|
||||
- Keep clearly V1-only events, such as `message.updated` and `message.part.*`, out of the current Protocol/SDK Next event surface unless a current-client requirement is documented.
|
||||
- Keep compatibility events available only to the existing App/TUI/CLI compatibility surface while they are still needed.
|
||||
- Preserve a single canonical event definition. Do not duplicate definitions for generation convenience.
|
||||
|
||||
## Module Shape
|
||||
|
||||
- Use one canonical exported value for each contract. Avoid bridge aliases such as `PluginID`, `PluginEvent`, `PtyInfo`, `PtyEvent`, and `SessionTodoInfo`.
|
||||
- Prefer importing the schema module namespace and reading canonical members, for example `Plugin.ID` or `SessionTodo.Info`.
|
||||
- Core may compose Schema contracts with runtime behavior into a deliberate domain facade, but the facade must re-export the exact canonical Schema value. Do not create a second schema identity.
|
||||
- Use flat top-level exports plus the package's existing namespace projection pattern, for example `export * as SessionMessage from "./session-message"`.
|
||||
- Keep standalone ID modules only when they prevent real cycles or heavy dependency edges. Inline one-off IDs into their owning contract module when no cycle exists.
|
||||
|
||||
## Naming
|
||||
|
||||
- Exported schema values and namespace objects use `PascalCase`.
|
||||
- Schema-building functions and combinators use `camelCase`.
|
||||
- The package's static-method combinator is `statics(...)`.
|
||||
- Keep descriptive schema value names such as `PositiveInt`, `NonNegativeInt`, `AbsolutePath`, `RelativePath`, and `DateTimeUtcFromMillis`.
|
||||
|
||||
## Optional Fields And Defaults
|
||||
|
||||
- Use the package `optional(...)` helper for optional object properties, including nested structs and event payloads, so encoded objects omit `undefined` keys.
|
||||
- Use raw `Schema.optional(...)` only when preserving `undefined` as an explicitly encoded property is intentional and documented.
|
||||
- External convenience defaults are normally decode-only with `Schema.withDecodingDefault(...)`.
|
||||
- Add constructor defaults only when the domain value itself requires construction-time normalization.
|
||||
|
||||
## Public Types
|
||||
|
||||
- Public `Schema.Struct` records use same-name interfaces:
|
||||
|
||||
```ts
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
export const Info = Schema.Struct({ ... })
|
||||
```
|
||||
|
||||
- Use type aliases for unions, scalars, arrays, branded scalar types, and event payload helper types.
|
||||
- Closed documented string sets use `Schema.Literals(...)`. If arbitrary strings are valid, document the field as arbitrary rather than listing a closed set.
|
||||
|
||||
## Mutability
|
||||
|
||||
- Public Schema contracts are readonly by default.
|
||||
- Do not use `Schema.mutable(...)` in public contracts for runtime convenience.
|
||||
- Runtime code that needs mutation should opt in at the boundary with `Types.DeepMutable`, a purpose-built draft type, or another explicit mutable API.
|
||||
|
||||
## Unknown Values
|
||||
|
||||
- Current public contracts avoid `Schema.Any`.
|
||||
- Use `Schema.Json` for values that must be JSON-serializable.
|
||||
- Use `Schema.Unknown` for genuinely opaque values that require consumer-side narrowing.
|
||||
- Keep `Schema.Any` only at an explicitly unsafe compatibility boundary with a documented reason.
|
||||
|
||||
## IDs And Identifiers
|
||||
|
||||
- Current ID constructors expose `create()`.
|
||||
- Directional constructors such as `ascending()` or `descending()` remain only where ordering semantics are part of the public contract or compatibility requires the old method.
|
||||
- New generated ID schemas must validate exactly the prefix they emit, including the underscore.
|
||||
- Do not tighten legacy loose ID validators without an explicit compatibility and migration decision; existing callers and tests may rely on accepted non-canonical IDs.
|
||||
- Reusable exported public schemas get stable, domain-qualified identifiers such as `Model.Ref` or `Agent.Color`.
|
||||
- Public schema identifiers and brands must be unique and stable. Private one-use nested schemas may remain anonymous.
|
||||
|
||||
## Tests For Contract Changes
|
||||
|
||||
- Add focused tests when changing contract behavior or generated surface.
|
||||
- Cover optional properties omitting `undefined`, no accidental current-contract `Schema.Any`, stable and unique public identifiers, exact facade/schema identity, and current Protocol manifests excluding V1-only events.
|
||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
||||
import { Model } from "./model"
|
||||
import { Permission } from "./permission"
|
||||
import { Provider } from "./provider"
|
||||
import { PositiveInt, withStatics } from "./schema"
|
||||
import { PositiveInt, statics } from "./schema"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID"))
|
||||
export type ID = typeof ID.Type
|
||||
@@ -30,7 +30,7 @@ export const Info = Schema.Struct({
|
||||
})
|
||||
.annotate({ identifier: "AgentV2.Info" })
|
||||
.pipe(
|
||||
withStatics((schema) => ({
|
||||
statics((schema) => ({
|
||||
empty: (id: ID) =>
|
||||
schema.make({ id, request: { headers: {}, body: {} }, mode: "all", hidden: false, permissions: [] }),
|
||||
})),
|
||||
|
||||
@@ -3,11 +3,11 @@ export * as Credential from "./credential"
|
||||
import { Schema } from "effect"
|
||||
import { IntegrationMethodID } from "./integration-id"
|
||||
import { ascending } from "./identifier"
|
||||
import { NonNegativeInt, withStatics } from "./schema"
|
||||
import { NonNegativeInt, statics } from "./schema"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("Credential.ID"),
|
||||
withStatics((schema) => ({ create: () => schema.make("cred_" + ascending()) })),
|
||||
statics((schema) => ({ create: () => schema.make("cred_" + ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
|
||||
@@ -3,70 +3,127 @@ export * as Event from "./event"
|
||||
import { Schema } from "effect"
|
||||
import { ascending } from "./identifier"
|
||||
import { Location } from "./location"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } from "./schema"
|
||||
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
|
||||
Schema.brand("Event.ID"),
|
||||
withStatics((schema) => ({ create: () => schema.make("evt_" + ascending()) })),
|
||||
statics((schema) => ({ create: () => schema.make("evt_" + ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export type Definition<
|
||||
export type DurableOptions = {
|
||||
readonly version: number
|
||||
readonly aggregate: string
|
||||
}
|
||||
|
||||
export type DurableEnvelope = {
|
||||
readonly aggregateID: string
|
||||
readonly seq: number
|
||||
readonly version: number
|
||||
}
|
||||
|
||||
const PublishedDurableEnvelope = Schema.Struct({
|
||||
aggregateID: Schema.String,
|
||||
seq: Schema.Number,
|
||||
version: Schema.Number,
|
||||
})
|
||||
const NoDurableEnvelope = Schema.optional(Schema.Never)
|
||||
|
||||
export type LiveDefinition<
|
||||
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
|
||||
readonly durable?: never
|
||||
}
|
||||
|
||||
export type DurableDefinition<
|
||||
Type extends string = string,
|
||||
DataSchema extends Schema.Codec<unknown, unknown> = Schema.Codec<unknown, unknown>,
|
||||
Durability extends DurableOptions = DurableOptions,
|
||||
> = Schema.Top & {
|
||||
readonly type: Type
|
||||
readonly data: DataSchema
|
||||
readonly durable: Durability
|
||||
}
|
||||
|
||||
export type Definition<
|
||||
Type extends string = string,
|
||||
DataSchema extends Schema.Codec<unknown, unknown> = Schema.Codec<unknown, unknown>,
|
||||
> = LiveDefinition<Type, DataSchema> | DurableDefinition<Type, DataSchema>
|
||||
|
||||
export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
|
||||
|
||||
export type Payload<D extends Definition = Definition> = {
|
||||
readonly id: ID
|
||||
readonly type: D["type"]
|
||||
readonly data: Data<D>
|
||||
readonly durable?: {
|
||||
readonly aggregateID: string
|
||||
readonly seq: number
|
||||
readonly version: number
|
||||
}
|
||||
readonly location?: Location.Ref
|
||||
readonly metadata?: Record<string, unknown>
|
||||
}
|
||||
export type UncommittedPayload<D extends Definition = Definition> = D extends Definition
|
||||
? {
|
||||
readonly id: ID
|
||||
readonly type: D["type"]
|
||||
readonly data: Data<D>
|
||||
readonly location?: Location.Ref
|
||||
readonly metadata?: Record<string, unknown>
|
||||
}
|
||||
: never
|
||||
|
||||
export type PublishedPayload<D extends Definition = Definition> = D extends DurableDefinition
|
||||
? UncommittedPayload<D> & { readonly durable: DurableEnvelope }
|
||||
: D extends LiveDefinition
|
||||
? UncommittedPayload<D> & { readonly durable?: never }
|
||||
: never
|
||||
|
||||
export type Payload<D extends Definition = Definition> = PublishedPayload<D>
|
||||
|
||||
type LiveEventSchema<
|
||||
Type extends string,
|
||||
Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
|
||||
> = Schema.Schema<PublishedPayload<LiveDefinition<Type, Schema.Struct<Fields>>>> &
|
||||
LiveDefinition<Type, Schema.Struct<Fields>>
|
||||
|
||||
type DurableEventSchema<
|
||||
Type extends string,
|
||||
Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
|
||||
Durability extends DurableOptions,
|
||||
> = Schema.Schema<PublishedPayload<DurableDefinition<Type, Schema.Struct<Fields>, Durability>>> &
|
||||
DurableDefinition<Type, Schema.Struct<Fields>, Durability>
|
||||
|
||||
export function define<
|
||||
const Type extends string,
|
||||
Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
|
||||
>(input: { readonly type: Type; readonly durable?: never; readonly schema: Fields }): LiveEventSchema<Type, Fields>
|
||||
export function define<
|
||||
const Type extends string,
|
||||
Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
|
||||
const Durability extends DurableOptions,
|
||||
>(input: {
|
||||
readonly type: Type
|
||||
readonly durable?: {
|
||||
readonly version: number
|
||||
readonly aggregate: string
|
||||
}
|
||||
readonly durable: Durability
|
||||
readonly schema: Fields
|
||||
}): Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> & Definition<Type, Schema.Struct<Fields>> {
|
||||
}): DurableEventSchema<Type, Fields, Durability>
|
||||
export function define(input: {
|
||||
readonly type: string
|
||||
readonly durable?: DurableOptions
|
||||
readonly schema: Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>
|
||||
}): Schema.Top {
|
||||
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>>
|
||||
const fields = {
|
||||
id: ID,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
type: Schema.Literal(input.type),
|
||||
location: Schema.optional(Location.Ref),
|
||||
data,
|
||||
}
|
||||
if (input.durable) {
|
||||
return Object.assign(
|
||||
Schema.Struct({ ...fields, durable: PublishedDurableEnvelope }).annotate({
|
||||
identifier: input.type,
|
||||
}),
|
||||
{ type: input.type, durable: input.durable, data },
|
||||
)
|
||||
}
|
||||
return Object.assign(Schema.Struct({ ...fields, durable: NoDurableEnvelope }).annotate({ identifier: input.type }), {
|
||||
type: input.type,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function inventory<const Definitions extends ReadonlyArray<Definition>>(...definitions: Definitions) {
|
||||
@@ -103,7 +160,7 @@ export function durable(definitions: ReadonlyArray<Definition>) {
|
||||
if (result.has(key)) throw new Error(`Duplicate durable event definition for ${key}`)
|
||||
result.set(key, definition)
|
||||
return result
|
||||
}, new Map<string, Definition>()),
|
||||
}, new Map<string, DurableDefinition>()),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { Connection } from "./connection"
|
||||
import { ascending } from "./identifier"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } from "./schema"
|
||||
import { IntegrationID, IntegrationMethodID } from "./integration-id"
|
||||
|
||||
export const ID = IntegrationID
|
||||
@@ -102,7 +102,7 @@ export class Info extends Schema.Class<Info>("Integration.Info")({
|
||||
|
||||
export const AttemptID = Schema.String.pipe(
|
||||
Schema.brand("Integration.AttemptID"),
|
||||
withStatics((schema) => ({ create: () => schema.make("con_" + ascending()) })),
|
||||
statics((schema) => ({ create: () => schema.make("con_" + ascending()) })),
|
||||
)
|
||||
export type AttemptID = typeof AttemptID.Type
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as Location from "./location"
|
||||
|
||||
import { Effect, Schema } from "effect"
|
||||
import { AbsolutePath, optionalOmitUndefined } from "./schema"
|
||||
import { AbsolutePath, optional } from "./schema"
|
||||
import { ProjectID } from "./project-id"
|
||||
import { WorkspaceID } from "./workspace-id"
|
||||
|
||||
@@ -16,7 +16,7 @@ export const Ref = Schema.Struct({
|
||||
|
||||
export class Info extends Schema.Class<Info>("Location.Info")({
|
||||
directory: AbsolutePath,
|
||||
workspaceID: optionalOmitUndefined(WorkspaceID),
|
||||
workspaceID: optional(WorkspaceID),
|
||||
project: Schema.Struct({
|
||||
id: ProjectID,
|
||||
directory: AbsolutePath,
|
||||
|
||||
@@ -2,7 +2,7 @@ export * as Model from "./model"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Provider } from "./provider"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } from "./schema"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID"))
|
||||
export type ID = typeof ID.Type
|
||||
@@ -83,7 +83,7 @@ export const Info = Schema.Struct({
|
||||
})
|
||||
.annotate({ identifier: "ModelV2.Info" })
|
||||
.pipe(
|
||||
withStatics((schema) => ({
|
||||
statics((schema) => ({
|
||||
empty: (providerID: Provider.ID, modelID: ID) =>
|
||||
schema.make({
|
||||
id: modelID,
|
||||
|
||||
@@ -3,11 +3,11 @@ export * as PermissionSaved from "./permission-saved"
|
||||
import { Schema } from "effect"
|
||||
import { ascending } from "./identifier"
|
||||
import { ProjectID } from "./project-id"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } from "./schema"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("PermissionSaved.ID"),
|
||||
withStatics((schema) => ({ create: () => schema.make("psv_" + ascending()) })),
|
||||
statics((schema) => ({ create: () => schema.make("psv_" + ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { ascending } from "./identifier"
|
||||
import { Project } from "./project"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } 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()) })),
|
||||
statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "per_" + ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
@@ -64,4 +64,3 @@ const Replied = define({
|
||||
schema: { sessionID: SessionID, requestID: ID, reply: Reply },
|
||||
})
|
||||
export const Event = { Asked, Replied, Definitions: inventory(Asked, Replied) }
|
||||
export const PermissionV1Event = Event
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { ascending } from "./identifier"
|
||||
import { SessionID } from "./session-id"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } 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()) })),
|
||||
statics((schema) => ({ create: (id?: string) => schema.make(id ?? "per_" + ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
|
||||
@@ -5,11 +5,9 @@ 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
|
||||
|
||||
@@ -8,4 +8,3 @@ const Updated = define({
|
||||
schema: { projectID: Project.ID },
|
||||
})
|
||||
export const Event = { Updated, Definitions: inventory(Updated) }
|
||||
export const ProjectDirectoriesEvent = Event
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Schema } from "effect"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } from "./schema"
|
||||
|
||||
export const ProjectID = Schema.String.pipe(
|
||||
Schema.brand("Project.ID"),
|
||||
withStatics((schema) => ({ global: schema.make("global") })),
|
||||
statics((schema) => ({ global: schema.make("global") })),
|
||||
)
|
||||
export type ProjectID = typeof ProjectID.Type
|
||||
|
||||
@@ -2,7 +2,7 @@ export * as Project from "./project"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { NonNegativeInt, optionalOmitUndefined } from "./schema"
|
||||
import { NonNegativeInt, optional } from "./schema"
|
||||
import { ProjectID } from "./project-id"
|
||||
|
||||
export const ID = ProjectID
|
||||
@@ -10,28 +10,28 @@ 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),
|
||||
url: optional(Schema.String),
|
||||
override: optional(Schema.String),
|
||||
color: optional(Schema.String),
|
||||
})
|
||||
export const Commands = Schema.Struct({
|
||||
start: optionalOmitUndefined(
|
||||
start: optional(
|
||||
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),
|
||||
initialized: optional(NonNegativeInt),
|
||||
})
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
id: ID,
|
||||
worktree: Schema.String,
|
||||
vcs: optionalOmitUndefined(Vcs),
|
||||
name: optionalOmitUndefined(Schema.String),
|
||||
icon: optionalOmitUndefined(Icon),
|
||||
commands: optionalOmitUndefined(Commands),
|
||||
vcs: optional(Vcs),
|
||||
name: optional(Schema.String),
|
||||
icon: optional(Icon),
|
||||
commands: optional(Commands),
|
||||
time: Time,
|
||||
sandboxes: Schema.Array(Schema.String),
|
||||
}).annotate({ identifier: "Project" })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } from "./schema"
|
||||
|
||||
export interface Source extends Schema.Schema.Type<typeof Source> {}
|
||||
export const Source = Schema.Struct({
|
||||
@@ -18,7 +18,7 @@ export const FileAttachment = Schema.Struct({
|
||||
})
|
||||
.annotate({ identifier: "Prompt.FileAttachment" })
|
||||
.pipe(
|
||||
withStatics((schema) => ({
|
||||
statics((schema) => ({
|
||||
create: (input: FileAttachment) =>
|
||||
schema.make({
|
||||
uri: input.uri,
|
||||
@@ -44,7 +44,7 @@ export const Prompt = Schema.Struct({
|
||||
})
|
||||
.annotate({ identifier: "Prompt" })
|
||||
.pipe(
|
||||
withStatics((schema) => ({
|
||||
statics((schema) => ({
|
||||
equivalence: Schema.toEquivalence(schema),
|
||||
fromUserMessage: (input: Pick<Prompt, "text" | "files" | "agents">) =>
|
||||
schema.make({
|
||||
|
||||
@@ -2,11 +2,11 @@ export * as Provider from "./provider"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Integration } from "./integration"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } from "./schema"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("ProviderV2.ID"),
|
||||
withStatics((schema) => ({
|
||||
statics((schema) => ({
|
||||
opencode: schema.make("opencode"),
|
||||
anthropic: schema.make("anthropic"),
|
||||
openai: schema.make("openai"),
|
||||
@@ -57,7 +57,7 @@ export const Info = Schema.Struct({
|
||||
})
|
||||
.annotate({ identifier: "ProviderV2.Info" })
|
||||
.pipe(
|
||||
withStatics((schema) => ({
|
||||
statics((schema) => ({
|
||||
empty: (id: ID) =>
|
||||
schema.make({
|
||||
id,
|
||||
|
||||
@@ -3,13 +3,12 @@ export * as Pty from "./pty"
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { ascending } from "./identifier"
|
||||
import { NonNegativeInt, PositiveInt } from "./schema"
|
||||
import { withStatics } from "./schema"
|
||||
import { NonNegativeInt, PositiveInt, statics } from "./schema"
|
||||
|
||||
const IDSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID"))
|
||||
|
||||
export const ID = IDSchema.pipe(
|
||||
withStatics((schema: typeof IDSchema) => ({
|
||||
statics((schema: typeof IDSchema) => ({
|
||||
ascending: (id?: string) => schema.make(id ?? "pty_" + ascending()),
|
||||
})),
|
||||
)
|
||||
@@ -25,14 +24,12 @@ export const Info = Schema.Struct({
|
||||
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
|
||||
|
||||
export const CreateInput = Schema.Struct({
|
||||
command: Schema.optional(Schema.String),
|
||||
|
||||
@@ -3,13 +3,13 @@ export * as QuestionV1 from "./question-v1"
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { ascending } from "./identifier"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } 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()) })),
|
||||
statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "que_" + ascending()) })),
|
||||
)
|
||||
|
||||
export const Option = Schema.Struct({
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { ascending } from "./identifier"
|
||||
import { SessionID } from "./session-id"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } 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()) })),
|
||||
statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "que_" + ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ export * as Revert from "./revert"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { NonNegativeInt, RelativePath } from "./schema"
|
||||
import { SessionMessageID } from "./session-message-id"
|
||||
import { SessionMessage } from "./session-message"
|
||||
|
||||
export const FileDiff = Schema.Struct({
|
||||
path: RelativePath,
|
||||
@@ -14,7 +14,7 @@ export const FileDiff = Schema.Struct({
|
||||
export type FileDiff = typeof FileDiff.Type
|
||||
|
||||
export const State = Schema.Struct({
|
||||
messageID: SessionMessageID.ID,
|
||||
messageID: SessionMessage.ID,
|
||||
partID: Schema.String.pipe(Schema.optional),
|
||||
snapshot: Schema.String.pipe(Schema.optional),
|
||||
diff: Schema.String.pipe(Schema.optional),
|
||||
|
||||
@@ -9,7 +9,7 @@ export type RelativePath = typeof RelativePath.Type
|
||||
export const AbsolutePath = Schema.String.pipe(Schema.brand("AbsolutePath"))
|
||||
export type AbsolutePath = typeof AbsolutePath.Type
|
||||
|
||||
export const optionalOmitUndefined = <S extends Schema.Top>(schema: S) =>
|
||||
export const optional = <S extends Schema.Top>(schema: S) =>
|
||||
Schema.optionalKey(schema).pipe(
|
||||
Schema.decodeTo(Schema.optional(schema), {
|
||||
decode: SchemaGetter.passthrough({ strict: false }),
|
||||
@@ -17,7 +17,7 @@ export const optionalOmitUndefined = <S extends Schema.Top>(schema: S) =>
|
||||
}),
|
||||
)
|
||||
|
||||
export const withStatics =
|
||||
export const statics =
|
||||
<S extends object, M extends Record<string, unknown>>(methods: (schema: S) => M) =>
|
||||
(schema: S): S & M =>
|
||||
Object.assign(schema, methods(schema))
|
||||
|
||||
@@ -9,7 +9,6 @@ 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"
|
||||
import { Revert } from "./revert"
|
||||
|
||||
@@ -30,7 +29,7 @@ const Base = {
|
||||
}
|
||||
const PromptFields = {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
messageID: SessionMessage.ID,
|
||||
prompt: Prompt,
|
||||
delivery: Delivery,
|
||||
}
|
||||
@@ -56,7 +55,7 @@ export const AgentSwitched = Event.define({
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
messageID: SessionMessage.ID,
|
||||
agent: Schema.String,
|
||||
},
|
||||
})
|
||||
@@ -67,7 +66,7 @@ export const ModelSwitched = Event.define({
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
messageID: SessionMessage.ID,
|
||||
model: Model.Ref,
|
||||
},
|
||||
})
|
||||
@@ -103,7 +102,7 @@ export const ContextUpdated = Event.define({
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
messageID: SessionMessage.ID,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
@@ -114,7 +113,7 @@ export const Synthetic = Event.define({
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
messageID: SessionMessage.ID,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
@@ -126,7 +125,7 @@ export namespace Shell {
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
messageID: SessionMessage.ID,
|
||||
callID: Schema.String,
|
||||
command: Schema.String,
|
||||
},
|
||||
@@ -151,7 +150,7 @@ export namespace Step {
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
agent: Schema.String,
|
||||
model: Model.Ref,
|
||||
snapshot: Schema.String.pipe(Schema.optional),
|
||||
@@ -164,7 +163,7 @@ export namespace Step {
|
||||
...stepSettlementOptions,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
finish: Schema.String,
|
||||
cost: Schema.Finite,
|
||||
tokens: Schema.Struct({
|
||||
@@ -187,7 +186,7 @@ export namespace Step {
|
||||
...stepSettlementOptions,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
error: UnknownError,
|
||||
},
|
||||
})
|
||||
@@ -200,7 +199,7 @@ export namespace Text {
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
textID: Schema.String,
|
||||
},
|
||||
})
|
||||
@@ -211,7 +210,7 @@ export namespace Text {
|
||||
type: "session.next.text.delta",
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
textID: Schema.String,
|
||||
delta: Schema.String,
|
||||
},
|
||||
@@ -223,7 +222,7 @@ export namespace Text {
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
textID: Schema.String,
|
||||
text: Schema.String,
|
||||
},
|
||||
@@ -237,7 +236,7 @@ export namespace Reasoning {
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
reasoningID: Schema.String,
|
||||
providerMetadata: ProviderMetadata.pipe(Schema.optional),
|
||||
},
|
||||
@@ -249,7 +248,7 @@ export namespace Reasoning {
|
||||
type: "session.next.reasoning.delta",
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
reasoningID: Schema.String,
|
||||
delta: Schema.String,
|
||||
},
|
||||
@@ -261,7 +260,7 @@ export namespace Reasoning {
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
reasoningID: Schema.String,
|
||||
text: Schema.String,
|
||||
providerMetadata: ProviderMetadata.pipe(Schema.optional),
|
||||
@@ -273,7 +272,7 @@ export namespace Reasoning {
|
||||
export namespace Tool {
|
||||
const ToolBase = {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
callID: Schema.String,
|
||||
}
|
||||
|
||||
@@ -401,7 +400,7 @@ export namespace Compaction {
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
messageID: SessionMessage.ID,
|
||||
reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]),
|
||||
},
|
||||
})
|
||||
@@ -411,7 +410,7 @@ export namespace Compaction {
|
||||
type: "session.next.compaction.delta",
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
messageID: SessionMessage.ID,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
@@ -422,7 +421,7 @@ export namespace Compaction {
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
messageID: SessionMessage.ID,
|
||||
reason: Started.data.fields.reason,
|
||||
text: Schema.String,
|
||||
recent: Schema.String,
|
||||
@@ -441,7 +440,7 @@ export namespace RevertEvent {
|
||||
export const Committed = Event.define({
|
||||
type: "session.next.revert.committed",
|
||||
...options,
|
||||
schema: { ...Base, messageID: SessionMessageID.ID },
|
||||
schema: { ...Base, messageID: SessionMessage.ID },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Schema } from "effect"
|
||||
import { descending } from "./identifier"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } from "./schema"
|
||||
|
||||
export const SessionID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
|
||||
Schema.brand("SessionID"),
|
||||
withStatics((schema) => {
|
||||
statics((schema) => {
|
||||
const create = () => schema.make("ses_" + descending())
|
||||
return {
|
||||
create,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Prompt } from "./prompt"
|
||||
import { DateTimeUtcFromMillis, NonNegativeInt } from "./schema"
|
||||
import { SessionDelivery } from "./session-delivery"
|
||||
import { SessionID } from "./session-id"
|
||||
import { SessionMessageID } from "./session-message-id"
|
||||
import { SessionMessage } from "./session-message"
|
||||
|
||||
export const Delivery = SessionDelivery.Delivery
|
||||
export type Delivery = SessionDelivery.Delivery
|
||||
@@ -13,7 +13,7 @@ export type Delivery = SessionDelivery.Delivery
|
||||
export interface Admitted extends Schema.Schema.Type<typeof Admitted> {}
|
||||
export const Admitted = Schema.Struct({
|
||||
admittedSeq: NonNegativeInt,
|
||||
id: SessionMessageID.ID,
|
||||
id: SessionMessage.ID,
|
||||
sessionID: SessionID,
|
||||
prompt: Prompt,
|
||||
delivery: Delivery,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export * as SessionMessageID from "./session-message-id"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { ascending } from "./identifier"
|
||||
import { withStatics } from "./schema"
|
||||
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe(
|
||||
Schema.brand("Session.Message.ID"),
|
||||
withStatics((schema) => ({ create: () => schema.make("msg_" + ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
@@ -4,12 +4,15 @@ import { Schema } from "effect"
|
||||
import { ProviderMetadata, ToolContent } from "./llm"
|
||||
import { Model } from "./model"
|
||||
import { FileAttachment, Prompt } from "./prompt"
|
||||
import { DateTimeUtcFromMillis, RelativePath } from "./schema"
|
||||
import { DateTimeUtcFromMillis, RelativePath, statics } from "./schema"
|
||||
import { SessionID } from "./session-id"
|
||||
import { SessionMessageID } from "./session-message-id"
|
||||
import { ascending } from "./identifier"
|
||||
|
||||
export const ID = SessionMessageID.ID
|
||||
export type ID = SessionMessageID.ID
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe(
|
||||
Schema.brand("Session.Message.ID"),
|
||||
statics((schema) => ({ create: () => schema.make("msg_" + ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export interface UnknownError extends Schema.Schema.Type<typeof UnknownError> {}
|
||||
export const UnknownError = Schema.Struct({
|
||||
|
||||
@@ -12,7 +12,6 @@ export const Info = Schema.Struct({
|
||||
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",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PermissionV1 } from "./permission-v1"
|
||||
import { Project } from "./project"
|
||||
import { Provider } from "./provider"
|
||||
import { Model } from "./model"
|
||||
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "./schema"
|
||||
import { NonNegativeInt, optional, statics } from "./schema"
|
||||
import { ascending } from "./identifier"
|
||||
import { SessionID } from "./session-id"
|
||||
import { WorkspaceID } from "./workspace-id"
|
||||
@@ -16,13 +16,13 @@ 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()) })),
|
||||
statics((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()) })),
|
||||
statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + ascending()) })),
|
||||
)
|
||||
export type PartID = typeof PartID.Type
|
||||
|
||||
@@ -510,7 +510,7 @@ const SessionSummary = Schema.Struct({
|
||||
additions: Schema.Finite,
|
||||
deletions: Schema.Finite,
|
||||
files: Schema.Finite,
|
||||
diffs: optionalOmitUndefined(Schema.Array(FileDiff.Info)),
|
||||
diffs: optional(Schema.Array(FileDiff.Info)),
|
||||
})
|
||||
|
||||
const SessionTokens = Schema.Struct({
|
||||
@@ -529,42 +529,42 @@ const SessionShare = Schema.Struct({
|
||||
|
||||
const SessionRevert = Schema.Struct({
|
||||
messageID: MessageID,
|
||||
partID: optionalOmitUndefined(PartID),
|
||||
snapshot: optionalOmitUndefined(Schema.String),
|
||||
diff: optionalOmitUndefined(Schema.String),
|
||||
partID: optional(PartID),
|
||||
snapshot: optional(Schema.String),
|
||||
diff: optional(Schema.String),
|
||||
})
|
||||
|
||||
const SessionModel = Schema.Struct({
|
||||
id: Model.ID,
|
||||
providerID: Provider.ID,
|
||||
variant: optionalOmitUndefined(Schema.String),
|
||||
variant: optional(Schema.String),
|
||||
})
|
||||
|
||||
export const SessionInfo = Schema.Struct({
|
||||
id: SessionID,
|
||||
slug: Schema.String,
|
||||
projectID: Project.ID,
|
||||
workspaceID: optionalOmitUndefined(WorkspaceID),
|
||||
workspaceID: optional(WorkspaceID),
|
||||
directory: Schema.String,
|
||||
path: optionalOmitUndefined(Schema.String),
|
||||
parentID: optionalOmitUndefined(SessionID),
|
||||
summary: optionalOmitUndefined(SessionSummary),
|
||||
cost: optionalOmitUndefined(Schema.Finite),
|
||||
tokens: optionalOmitUndefined(SessionTokens),
|
||||
share: optionalOmitUndefined(SessionShare),
|
||||
path: optional(Schema.String),
|
||||
parentID: optional(SessionID),
|
||||
summary: optional(SessionSummary),
|
||||
cost: optional(Schema.Finite),
|
||||
tokens: optional(SessionTokens),
|
||||
share: optional(SessionShare),
|
||||
title: Schema.String,
|
||||
agent: optionalOmitUndefined(Schema.String),
|
||||
model: optionalOmitUndefined(SessionModel),
|
||||
agent: optional(Schema.String),
|
||||
model: optional(SessionModel),
|
||||
version: Schema.String,
|
||||
metadata: optionalOmitUndefined(Schema.Record(Schema.String, Schema.Any)),
|
||||
metadata: optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
updated: NonNegativeInt,
|
||||
compacting: optionalOmitUndefined(NonNegativeInt),
|
||||
archived: optionalOmitUndefined(Schema.Finite),
|
||||
compacting: optional(NonNegativeInt),
|
||||
archived: optional(Schema.Finite),
|
||||
}),
|
||||
permission: optionalOmitUndefined(PermissionV1.Ruleset),
|
||||
revert: optionalOmitUndefined(SessionRevert),
|
||||
permission: optional(PermissionV1.Ruleset),
|
||||
revert: optional(SessionRevert),
|
||||
}).annotate({ identifier: "Session" })
|
||||
export type SessionInfo = typeof SessionInfo.Type
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Agent } from "./agent"
|
||||
import { Location } from "./location"
|
||||
import { Model } from "./model"
|
||||
import { Project } from "./project"
|
||||
import { DateTimeUtcFromMillis, optionalOmitUndefined, RelativePath } from "./schema"
|
||||
import { DateTimeUtcFromMillis, optional, RelativePath } from "./schema"
|
||||
import { SessionEvent } from "./session-event"
|
||||
import { SessionID } from "./session-id"
|
||||
import { Revert } from "./revert"
|
||||
@@ -18,7 +18,7 @@ export const Event = SessionEvent
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
export const Info = Schema.Struct({
|
||||
id: ID,
|
||||
parentID: ID.pipe(optionalOmitUndefined),
|
||||
parentID: ID.pipe(optional),
|
||||
projectID: Project.ID,
|
||||
agent: Agent.ID.pipe(Schema.optional),
|
||||
model: Model.Ref.pipe(Schema.optional),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Schema } from "effect"
|
||||
import { ascending } from "./identifier"
|
||||
import { withStatics } from "./schema"
|
||||
import { statics } from "./schema"
|
||||
|
||||
export const WorkspaceID = Schema.String.check(Schema.isStartsWith("wrk")).pipe(
|
||||
Schema.brand("WorkspaceV2.ID"),
|
||||
withStatics((schema) => {
|
||||
statics((schema) => {
|
||||
const create = () => schema.make("wrk_" + ascending())
|
||||
return {
|
||||
ascending: (id?: string) => {
|
||||
|
||||
@@ -34,4 +34,96 @@ describe("public event schemas", () => {
|
||||
|
||||
expect(Event.durable([definition]).get("test.durable.1")).toBe(definition)
|
||||
})
|
||||
|
||||
test("durable definitions require published commit metadata", () => {
|
||||
const definition = Event.define({
|
||||
type: "test.durable",
|
||||
durable: { aggregate: "id", version: 1 },
|
||||
schema: { id: Schema.String },
|
||||
})
|
||||
const payload: typeof definition.Type = {
|
||||
id: Event.ID.create(),
|
||||
type: definition.type,
|
||||
durable: { aggregateID: "aggregate", seq: 0, version: 1 },
|
||||
data: { id: "aggregate" },
|
||||
}
|
||||
|
||||
expect(Schema.is(definition)(payload)).toBe(true)
|
||||
expect(
|
||||
Schema.is(definition)({
|
||||
id: Event.ID.create(),
|
||||
type: definition.type,
|
||||
data: { id: "aggregate" },
|
||||
}),
|
||||
).toBe(false)
|
||||
// @ts-expect-error Published durable payloads require commit metadata.
|
||||
const missing: typeof definition.Type = { id: Event.ID.create(), type: definition.type, data: { id: "aggregate" } }
|
||||
void missing
|
||||
})
|
||||
|
||||
test("live definitions reject durable commit metadata", () => {
|
||||
const definition = Event.define({
|
||||
type: "test.live",
|
||||
schema: { value: Schema.String },
|
||||
})
|
||||
const payload: typeof definition.Type = {
|
||||
id: Event.ID.create(),
|
||||
type: definition.type,
|
||||
data: { value: "value" },
|
||||
}
|
||||
|
||||
expect(Schema.is(definition)(payload)).toBe(true)
|
||||
expect(
|
||||
Schema.is(definition)({
|
||||
...payload,
|
||||
durable: { aggregateID: "aggregate", seq: 0, version: 1 },
|
||||
}),
|
||||
).toBe(false)
|
||||
|
||||
const invalid: typeof definition.Type = {
|
||||
...payload,
|
||||
// @ts-expect-error Live payloads cannot carry durable commit metadata.
|
||||
durable: { aggregateID: "aggregate", seq: 0, version: 1 },
|
||||
}
|
||||
void invalid
|
||||
})
|
||||
|
||||
test("mixed definition payloads preserve durability correlation", () => {
|
||||
const durable = Event.define({
|
||||
type: "test.mixed.durable",
|
||||
durable: { aggregate: "id", version: 2 },
|
||||
schema: { id: Schema.String },
|
||||
})
|
||||
const live = Event.define({
|
||||
type: "test.mixed.live",
|
||||
schema: { value: Schema.String },
|
||||
})
|
||||
type Mixed = Event.Payload<typeof durable | typeof live>
|
||||
|
||||
const committed: Mixed = {
|
||||
id: Event.ID.create(),
|
||||
type: durable.type,
|
||||
durable: { aggregateID: "aggregate", seq: 0, version: 2 },
|
||||
data: { id: "aggregate" },
|
||||
}
|
||||
const ephemeral: Mixed = {
|
||||
id: Event.ID.create(),
|
||||
type: live.type,
|
||||
data: { value: "value" },
|
||||
}
|
||||
void committed
|
||||
void ephemeral
|
||||
|
||||
// @ts-expect-error Durable union members require commit metadata.
|
||||
const uncommitted: Mixed = { id: Event.ID.create(), type: durable.type, data: { id: "aggregate" } }
|
||||
// @ts-expect-error Live union members cannot carry durable commit metadata.
|
||||
const falselyCommitted: Mixed = {
|
||||
id: Event.ID.create(),
|
||||
type: live.type,
|
||||
durable: { aggregateID: "aggregate", seq: 0, version: 2 },
|
||||
data: { value: "value" },
|
||||
}
|
||||
void uncommitted
|
||||
void falselyCommitted
|
||||
})
|
||||
})
|
||||
|
||||
@@ -58,6 +58,17 @@ if (sseTypesPatched === sseTypesSource) {
|
||||
}
|
||||
await Bun.write(sseTypesPath, sseTypesPatched)
|
||||
|
||||
// OpenAPI represents Schema.Never as `not: {}`, which @hey-api currently
|
||||
// widens to unknown. Preserve impossible optional event fields as never.
|
||||
const eventTypesPath = "./src/v2/gen/types.gen.ts"
|
||||
const eventTypesFile = Bun.file(eventTypesPath)
|
||||
const eventTypesSource = await eventTypesFile.text()
|
||||
const eventTypesPatched = eventTypesSource.replaceAll(" durable?: unknown", " durable?: never")
|
||||
if (eventTypesPatched === eventTypesSource) {
|
||||
throw new Error(`Event never patch did not apply; @hey-api/openapi-ts output may have changed (${eventTypesPath})`)
|
||||
}
|
||||
await Bun.write(eventTypesPath, eventTypesPatched)
|
||||
|
||||
await $`bun prettier --write src/gen`
|
||||
await $`bun prettier --write src/v2`
|
||||
await $`rm -rf dist`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -92,7 +92,7 @@ Use Effect schemas as the public contract:
|
||||
- branded schemas for ids
|
||||
- `Schema.Class` or `Schema.Struct` for domain data
|
||||
- `Schema.TaggedErrorClass` for expected errors
|
||||
- existing core helpers like `DeepMutable`, `withStatics`, and integer schemas where appropriate
|
||||
- existing core helpers like `DeepMutable`, `statics`, and integer schemas where appropriate
|
||||
|
||||
Prefer `Info` objects as the stored domain records. Add static `empty(...)` constructors when update APIs need to create records on first mutation.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
```ts
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("ProviderV2.ID"),
|
||||
withStatics((schema) => ({
|
||||
statics((schema) => ({
|
||||
opencode: schema.make("opencode"),
|
||||
anthropic: schema.make("anthropic"),
|
||||
openai: schema.make("openai"),
|
||||
|
||||
Reference in New Issue
Block a user