Merge remote-tracking branch 'origin/v2' into search-integration

# Conflicts:
#	packages/client/src/promise/generated/client.ts
#	packages/client/src/promise/generated/types.ts
#	packages/client/test/promise.test.ts
#	packages/core/src/plugin/host.ts
#	packages/core/src/plugin/internal.ts
#	packages/core/src/plugin/promise.ts
#	packages/core/test/plugin/host.ts
#	packages/plugin/src/v2/effect/index.ts
#	packages/plugin/src/v2/effect/integration.ts
#	packages/plugin/src/v2/promise/index.ts
#	packages/plugin/src/v2/promise/integration.ts
#	packages/protocol/src/client.ts
#	packages/schema/src/index.ts
#	packages/sdk/js/src/v2/gen/sdk.gen.ts
This commit is contained in:
Shoubhit Dash 2026-07-08 15:09:39 +05:30
commit a6acc2397d
374 changed files with 15254 additions and 10009 deletions

View file

@ -10,9 +10,12 @@ import { PositiveInt, statics } from "./schema.js"
const Updated = ephemeral({ type: "agent.updated", schema: {} })
export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID"))
export const ID = Schema.String.pipe(Schema.brand("Agent.ID"))
export type ID = typeof ID.Type
export const Name = Schema.String.pipe(Schema.brand("Agent.Name"))
export type Name = typeof Name.Type
export const Color = Schema.Union([
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
@ -22,6 +25,7 @@ export type Color = typeof Color.Type
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({
id: ID,
name: Name,
model: Model.Ref.pipe(optional),
request: Provider.Request,
system: Schema.String.pipe(optional),
@ -32,12 +36,13 @@ export const Info = Schema.Struct({
steps: PositiveInt.pipe(optional),
permissions: Permission.Ruleset,
})
.annotate({ identifier: "AgentV2.Info" })
.annotate({ identifier: "Agent.Info" })
.pipe(
statics((schema) => ({
empty: (id: ID) =>
schema.make({
id,
name: Name.make(id),
request: { settings: {}, headers: {}, body: {} },
mode: "all",
hidden: false,

View file

@ -4,6 +4,7 @@ import { Schema } from "effect"
import { ephemeral, inventory } from "./event.js"
import { optional } from "./schema.js"
import { Model } from "./model.js"
import { Agent } from "./agent.js"
const Updated = ephemeral({ type: "command.updated", schema: {} })
@ -12,10 +13,10 @@ export const Info = Schema.Struct({
name: Schema.String,
template: Schema.String,
description: Schema.String.pipe(optional),
agent: Schema.String.pipe(optional),
agent: Agent.ID.pipe(optional),
model: Model.Ref.pipe(optional),
subtask: Schema.Boolean.pipe(optional),
}).annotate({ identifier: "CommandV2.Info" })
}).annotate({ identifier: "Command.Info" })
export const Event = {
Updated,

View file

@ -80,6 +80,7 @@ export const ServerDefinitions = Event.inventory(
...InstallationEvent.Definitions,
...VcsEvent.Definitions,
McpEvent.StatusChanged,
McpEvent.ResourcesChanged,
// Shared transitional: V1 contracts the current TUI still consumes during
// the migration (permission.asked/replied, question.asked, session.error).
// Remove when the TUI moves to the current permission/question surfaces.

View file

@ -1,6 +1,6 @@
export * as Event from "./event.js"
import { Schema } from "effect"
import { Schema, SchemaTransformation } from "effect"
import { optional } from "./schema.js"
import { ascending } from "./identifier.js"
import { Location } from "./location.js"
@ -72,6 +72,7 @@ export type Payload<D extends Definition = Definition> = D extends DurableDefini
type Input<Type extends string, Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>> = {
readonly type: Type
readonly identifier?: string
readonly durable?: {
readonly version: number
readonly aggregate: string
@ -84,16 +85,29 @@ export function durable<
const Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
>(input: Input<Type, Fields> & { readonly durable: NonNullable<Input<Type, Fields>["durable"]> }) {
const data = Schema.Struct(input.schema)
const durable = Schema.Struct({
aggregateID: DurableEnvelope.fields.aggregateID,
seq: DurableEnvelope.fields.seq,
version: Schema.Literal(input.durable.version).pipe(
Schema.decodeTo(
Schema.toType(Version),
SchemaTransformation.transform({
decode: () => Version.make(input.durable.version),
encode: () => input.durable.version,
}),
),
),
})
return Schema.Struct({
id: ID,
created: DateTimeUtcFromMillis,
metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
type: Schema.Literal(input.type),
durable: DurableEnvelope,
durable,
location: optional(Location.Ref),
data,
})
.annotate({ identifier: input.type })
.annotate({ identifier: input.identifier ?? input.type })
.pipe(
statics(() => ({
type: input.type,
@ -117,7 +131,7 @@ export function ephemeral<
location: optional(Location.Ref),
data,
})
.annotate({ identifier: input.type })
.annotate({ identifier: input.identifier ?? input.type })
.pipe(
statics(() => ({
type: input.type,

View file

@ -1,13 +1,23 @@
export * as FileDiff from "./file-diff.js"
import { Schema } from "effect"
import { optional } from "./schema.js"
import { NonNegativeInt, optional } from "./schema.js"
export const Info = Schema.Struct({
file: optional(Schema.String),
patch: optional(Schema.String),
file: Schema.String,
patch: Schema.String,
additions: NonNegativeInt,
deletions: NonNegativeInt,
status: Schema.Literals(["added", "deleted", "modified"]),
}).annotate({ identifier: "FileDiff.Info" })
export interface Info extends Schema.Schema.Type<typeof Info> {}
/** V1 snapshot and persisted session diff shape. */
export const LegacyInfo = Schema.Struct({
file: Schema.String.pipe(optional),
patch: Schema.String.pipe(optional),
additions: Schema.Finite,
deletions: Schema.Finite,
status: optional(Schema.Literals(["added", "deleted", "modified"])),
}).annotate({ identifier: "SnapshotFileDiff" })
export interface Info extends Schema.Schema.Type<typeof Info> {}
status: Schema.Literals(["added", "deleted", "modified"]).pipe(optional),
}).annotate({ identifier: "FileDiff.LegacyInfo" })
export interface LegacyInfo extends Schema.Schema.Type<typeof LegacyInfo> {}

View file

@ -9,7 +9,9 @@ export { Form } from "./form.js"
export { Integration } from "./integration.js"
export { LLM } from "./llm.js"
export { Location } from "./location.js"
export { Mcp } from "./mcp.js"
export { Model } from "./model.js"
export { Money } from "./money.js"
export { Permission } from "./permission.js"
export { PermissionSaved } from "./permission-saved.js"
export { Project } from "./project.js"
@ -17,14 +19,15 @@ export { ProjectCopy } from "./project-copy.js"
export { Provider } from "./provider.js"
export { Reference } from "./reference.js"
export { Search } from "./search.js"
export { Revert } from "./revert.js"
export { Session } from "./session.js"
export { Vcs } from "./vcs.js"
export { SessionInput } from "./session-input.js"
export { SessionError } from "./session-error.js"
export { SessionMessage } from "./session-message.js"
export { Snapshot } from "./snapshot.js"
export { Shell } from "./shell.js"
export { Skill } from "./skill.js"
export { TokenUsage } from "./token-usage.js"
export { Pty } from "./pty.js"
export { PtyTicket } from "./pty-ticket.js"
export { Question } from "./question.js"

View file

@ -10,6 +10,13 @@ export const ToolsChanged = Event.ephemeral({
},
})
export const ResourcesChanged = Event.ephemeral({
type: "mcp.resources.changed",
schema: {
server: Schema.String,
},
})
export const BrowserOpenFailed = Event.ephemeral({
type: "mcp.browser.open.failed",
schema: {
@ -27,4 +34,4 @@ export const StatusChanged = Event.ephemeral({
},
})
export const Definitions = Event.inventory(ToolsChanged, StatusChanged)
export const Definitions = Event.inventory(ToolsChanged, ResourcesChanged, StatusChanged)

View file

@ -25,14 +25,9 @@ const NeedsClientRegistration = Schema.Struct({
}).annotate({ identifier: "Mcp.Status.NeedsClientRegistration" })
export type Status = typeof Status.Type
export const Status = Schema.Union([
Connected,
Pending,
Disabled,
Failed,
NeedsAuth,
NeedsClientRegistration,
]).pipe(Schema.toTaggedUnion("status"))
export const Status = Schema.Union([Connected, Pending, Disabled, Failed, NeedsAuth, NeedsClientRegistration]).pipe(
Schema.toTaggedUnion("status"),
)
export interface Server extends Schema.Schema.Type<typeof Server> {}
export const Server = Schema.Struct({
@ -42,3 +37,50 @@ export const Server = Schema.Struct({
// without matching by name, which could collide with provider or plugin integrations.
integrationID: optional(IntegrationID),
}).annotate({ identifier: "Mcp.Server" })
export interface Resource extends Schema.Schema.Type<typeof Resource> {}
export const Resource = Schema.Struct({
server: Schema.String,
name: Schema.String,
uri: Schema.String,
description: optional(Schema.String),
mimeType: optional(Schema.String),
}).annotate({ identifier: "Mcp.Resource" })
export interface ResourceTemplate extends Schema.Schema.Type<typeof ResourceTemplate> {}
export const ResourceTemplate = Schema.Struct({
server: Schema.String,
name: Schema.String,
uriTemplate: Schema.String,
description: optional(Schema.String),
mimeType: optional(Schema.String),
}).annotate({ identifier: "Mcp.ResourceTemplate" })
export interface ResourceCatalog extends Schema.Schema.Type<typeof ResourceCatalog> {}
export const ResourceCatalog = Schema.Struct({
resources: Schema.Array(Resource),
templates: Schema.Array(ResourceTemplate),
}).annotate({ identifier: "Mcp.ResourceCatalog" })
export const ResourceContentPart = Schema.Union([
Schema.Struct({
type: Schema.Literal("text"),
uri: Schema.String,
text: Schema.String,
mimeType: optional(Schema.String),
}),
Schema.Struct({
type: Schema.Literal("blob"),
uri: Schema.String,
blob: Schema.String,
mimeType: optional(Schema.String),
}),
]).pipe(Schema.toTaggedUnion("type"), Schema.annotate({ identifier: "Mcp.ResourceContentPart" }))
export type ResourceContentPart = typeof ResourceContentPart.Type
export interface ResourceContent extends Schema.Schema.Type<typeof ResourceContent> {}
export const ResourceContent = Schema.Struct({
server: Schema.String,
uri: Schema.String,
contents: Schema.Array(ResourceContentPart),
}).annotate({ identifier: "Mcp.ResourceContent" })

View file

@ -3,11 +3,12 @@ export * as Model from "./model.js"
import { Schema } from "effect"
import { optional, statics } from "./schema.js"
import { Provider } from "./provider.js"
import { Money } from "./money.js"
export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID"))
export const ID = Schema.String.pipe(Schema.brand("Model.ID"))
export type ID = typeof ID.Type
export const VariantID = Schema.String.pipe(Schema.brand("VariantID"))
export const VariantID = Schema.String.pipe(Schema.brand("Model.VariantID"))
export type VariantID = typeof VariantID.Type
export const Ref = Schema.Struct({
@ -17,7 +18,7 @@ export const Ref = Schema.Struct({
}).annotate({ identifier: "Model.Ref" })
export interface Ref extends Schema.Schema.Type<typeof Ref> {}
export const Family = Schema.String.pipe(Schema.brand("Family"))
export const Family = Schema.String.pipe(Schema.brand("Model.Family"))
export type Family = typeof Family.Type
export interface Capabilities extends Schema.Schema.Type<typeof Capabilities> {}
@ -30,14 +31,14 @@ export const Capabilities = Schema.Struct({
export interface Cost extends Schema.Schema.Type<typeof Cost> {}
export const Cost = Schema.Struct({
tier: Schema.Struct({
type: Schema.Literal("context"),
type: Schema.tag("context"),
size: Schema.Int,
}).pipe(optional),
input: Schema.Finite,
output: Schema.Finite,
input: Money.USDPerMillionTokens,
output: Money.USDPerMillionTokens,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
read: Money.USDPerMillionTokens,
write: Money.USDPerMillionTokens,
}),
}).annotate({ identifier: "Model.Cost" })
@ -70,7 +71,7 @@ export const Info = Schema.Struct({
output: Schema.Int,
}),
})
.annotate({ identifier: "ModelV2.Info" })
.annotate({ identifier: "Model.Info" })
.pipe(
statics((schema) => ({
empty: (providerID: Provider.ID, id: ID) =>

View file

@ -0,0 +1,18 @@
export * as Money from "./money.js"
import { Schema } from "effect"
import { statics } from "./schema.js"
export const USD = Schema.Finite.pipe(
Schema.brand("Money.USD"),
Schema.annotate({ identifier: "Money.USD" }),
statics((schema) => ({ zero: schema.make(0) })),
)
export type USD = typeof USD.Type
export const USDPerMillionTokens = Schema.Finite.pipe(
Schema.brand("Money.USDPerMillionTokens"),
Schema.annotate({ identifier: "Money.USDPerMillionTokens" }),
statics((schema) => ({ zero: schema.make(0) })),
)
export type USDPerMillionTokens = typeof USDPerMillionTokens.Type

View file

@ -1,24 +0,0 @@
export * as Revert from "./revert.js"
import { Schema } from "effect"
import { optional } from "./schema.js"
import { NonNegativeInt, RelativePath } from "./schema.js"
import { SessionMessage } from "./session-message.js"
export const FileDiff = Schema.Struct({
path: RelativePath,
status: Schema.Literals(["added", "modified", "deleted"]),
additions: NonNegativeInt,
deletions: NonNegativeInt,
patch: Schema.String,
}).annotate({ identifier: "File.Diff" })
export interface FileDiff extends Schema.Schema.Type<typeof FileDiff> {}
export const State = Schema.Struct({
messageID: SessionMessage.ID,
partID: Schema.String.pipe(optional),
snapshot: Schema.String.pipe(optional),
diff: Schema.String.pipe(optional),
files: Schema.Array(FileDiff).pipe(optional),
}).annotate({ identifier: "Revert.State" })
export interface State extends Schema.Schema.Type<typeof State> {}

View file

@ -12,9 +12,14 @@ import { FileAttachment, Prompt } from "./prompt.js"
import { SessionID } from "./session-id.js"
import { Location } from "./location.js"
import { SessionMessage } from "./session-message.js"
import { Revert } from "./revert.js"
import { Revert } from "./session-revert.js"
import { Shell as ShellSchema } from "./shell.js"
import { SessionError } from "./session-error.js"
import { Agent } from "./agent.js"
import { Skill as SkillSchema } from "./skill.js"
import { Money } from "./money.js"
import { Snapshot } from "./snapshot.js"
import { TokenUsage } from "./token-usage.js"
export { FileAttachment }
@ -23,7 +28,7 @@ export const Source = Schema.Struct({
end: NonNegativeInt,
text: Schema.String,
}).annotate({
identifier: "session.event.source",
identifier: "Session.Event.Source",
})
export interface Source extends Schema.Schema.Type<typeof Source> {}
@ -48,7 +53,7 @@ export const AgentSelected = Event.durable({
...options,
schema: {
...Base,
agent: Schema.String,
agent: Agent.ID,
},
})
export type AgentSelected = typeof AgentSelected.Type
@ -84,6 +89,16 @@ export const Renamed = Event.durable({
})
export type Renamed = typeof Renamed.Type
export const UsageUpdated = Event.ephemeral({
type: "session.usage.updated",
schema: {
...Base,
cost: Money.USD,
tokens: TokenUsage.Info,
},
})
export type UsageUpdated = typeof UsageUpdated.Type
export const Deleted = Event.durable({
type: "session.deleted",
durable: {
@ -172,7 +187,8 @@ export namespace Skill {
...options,
schema: {
...Base,
name: Schema.String,
id: SkillSchema.ID,
name: SkillSchema.Name,
text: Schema.String,
},
})
@ -209,9 +225,9 @@ export namespace Step {
schema: {
...Base,
assistantMessageID: SessionMessage.ID,
agent: Schema.String,
agent: Agent.ID,
model: Model.Ref,
snapshot: Schema.String.pipe(optional),
snapshot: Snapshot.ID.pipe(optional),
},
})
export type Started = typeof Started.Type
@ -223,17 +239,9 @@ export namespace Step {
...Base,
assistantMessageID: SessionMessage.ID,
finish: FinishReason,
cost: Schema.Finite,
tokens: Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
snapshot: Schema.String.pipe(optional),
cost: Money.USD,
tokens: TokenUsage.Info,
snapshot: Snapshot.ID.pipe(optional),
files: Schema.Array(RelativePath).pipe(optional),
},
})
@ -246,6 +254,8 @@ export namespace Step {
...Base,
assistantMessageID: SessionMessage.ID,
error: SessionError.Error,
cost: Money.USD.pipe(optional),
tokens: TokenUsage.Info.pipe(optional),
},
})
export type Failed = typeof Failed.Type
@ -400,7 +410,6 @@ export namespace Tool {
...ToolBase,
structured: Schema.Record(Schema.String, Schema.Unknown),
content: Schema.Array(ToolContent),
outputPaths: Schema.Array(Schema.String).pipe(optional),
result: Schema.Unknown.pipe(optional),
executed: Schema.Boolean,
resultState: SessionMessage.ProviderState.pipe(optional),
@ -451,7 +460,9 @@ export namespace Compaction {
...options,
schema: {
...Base,
reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]),
reason: Schema.Literals(["auto", "manual"]),
recent: Schema.String,
inputID: SessionMessage.ID.pipe(optional),
},
})
export type Started = typeof Started.Type
@ -480,7 +491,12 @@ export namespace Compaction {
export const Failed = Event.durable({
type: "session.compaction.failed",
...options,
schema: Base,
schema: {
...Base,
reason: Started.data.fields.reason,
error: SessionError.Error,
inputID: SessionMessage.ID.pipe(optional),
},
})
export type Failed = typeof Failed.Type
}
@ -489,7 +505,7 @@ export namespace RevertEvent {
export const Staged = Event.durable({
type: "session.revert.staged",
...options,
schema: { ...Base, revert: Revert.State },
schema: { ...Base, revert: Revert },
})
export const Cleared = Event.durable({ type: "session.revert.cleared", ...options, schema: Base })
export const Committed = Event.durable({
@ -504,6 +520,7 @@ export const Definitions = Event.inventory(
ModelSelected,
Moved,
Renamed,
UsageUpdated,
Deleted,
Forked,
PromptPromoted,
@ -550,7 +567,7 @@ export const DurableDefinitions = Event.inventory(
export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" })
.pipe(Schema.toTaggedUnion("type"))
.annotate({ identifier: "SessionDurableEvent" })
.annotate({ identifier: "Session.Event.Durable" })
export type DurableEvent = typeof Durable.Type
export const All = Schema.Union(Definitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))

View file

@ -24,13 +24,13 @@ export const Admitted = Schema.Struct({
export interface PromptEntry extends Schema.Schema.Type<typeof PromptEntry> {}
export const PromptEntry = Schema.Struct({
type: Schema.Literal("prompt"),
type: Schema.tag("prompt"),
...Admitted.fields,
}).annotate({ identifier: "SessionInput.PromptEntry" })
export interface Compaction extends Schema.Schema.Type<typeof Compaction> {}
export const Compaction = Schema.Struct({
type: Schema.Literal("compaction"),
type: Schema.tag("compaction"),
admittedSeq: NonNegativeInt,
id: SessionMessage.ID,
sessionID: SessionID,
@ -38,5 +38,8 @@ export const Compaction = Schema.Struct({
handledSeq: NonNegativeInt.pipe(optional),
}).annotate({ identifier: "SessionInput.Compaction" })
export const Entry = Schema.Union([PromptEntry, Compaction]).pipe(Schema.toTaggedUnion("type"))
export type Entry = typeof Entry.Type
export const Info = Schema.Union([PromptEntry, Compaction]).pipe(
Schema.toTaggedUnion("type"),
Schema.annotate({ identifier: "SessionInput.Info" }),
)
export type Info = typeof Info.Type

View file

@ -4,14 +4,18 @@ import { Schema } from "effect"
import { optional } from "./schema.js"
import { ToolContent } from "./llm.js"
import { Model } from "./model.js"
import { FileAttachment, Prompt } from "./prompt.js"
import { Prompt } from "./prompt.js"
import { DateTimeUtcFromMillis, PositiveInt, RelativePath, statics } from "./schema.js"
import { SessionID } from "./session-id.js"
import { ascending } from "./identifier.js"
import { Event } from "./event.js"
import { Shell as ShellSchema } from "./shell.js"
import { FinishReason } from "./llm.js"
import { SessionError } from "./session-error.js"
import { Agent } from "./agent.js"
import { Skill as SkillSchema } from "./skill.js"
import { Money } from "./money.js"
import { Snapshot } from "./snapshot.js"
import { TokenUsage } from "./token-usage.js"
export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe(
Schema.brand("Session.Message.ID"),
@ -36,14 +40,14 @@ export type ProviderState = typeof ProviderState.Type
export interface AgentSelected extends Schema.Schema.Type<typeof AgentSelected> {}
export const AgentSelected = Schema.Struct({
...Base,
type: Schema.Literal("agent-switched"),
agent: Schema.String,
type: Schema.tag("agent-switched"),
agent: Agent.ID,
}).annotate({ identifier: "Session.Message.AgentSelected" })
export interface ModelSelected extends Schema.Schema.Type<typeof ModelSelected> {}
export const ModelSelected = Schema.Struct({
...Base,
type: Schema.Literal("model-switched"),
type: Schema.tag("model-switched"),
model: Model.Ref,
previous: Model.Ref.pipe(optional),
}).annotate({ identifier: "Session.Message.ModelSelected" })
@ -54,38 +58,41 @@ export const User = Schema.Struct({
text: Prompt.fields.text,
files: Prompt.fields.files,
agents: Prompt.fields.agents,
type: Schema.Literal("user"),
type: Schema.tag("user"),
}).annotate({ identifier: "Session.Message.User" })
export interface Synthetic extends Schema.Schema.Type<typeof Synthetic> {}
export const Synthetic = Schema.Struct({
...Base,
sessionID: SessionID,
text: Schema.String,
description: Schema.String.pipe(optional),
type: Schema.Literal("synthetic"),
type: Schema.tag("synthetic"),
}).annotate({ identifier: "Session.Message.Synthetic" })
export interface System extends Schema.Schema.Type<typeof System> {}
export const System = Schema.Struct({
...Base,
type: Schema.Literal("system"),
type: Schema.tag("system"),
text: Schema.String,
}).annotate({ identifier: "Session.Message.System" })
export interface Skill extends Schema.Schema.Type<typeof Skill> {}
export const Skill = Schema.Struct({
...Base,
type: Schema.Literal("skill"),
name: Schema.String,
type: Schema.tag("skill"),
skill: SkillSchema.ID,
name: SkillSchema.Name,
text: Schema.String,
}).annotate({ identifier: "Session.Message.Skill" })
export interface Shell extends Schema.Schema.Type<typeof Shell> {}
export const Shell = Schema.Struct({
...Base,
type: Schema.Literal("shell"),
shell: ShellSchema.Info,
type: Schema.tag("shell"),
shellID: ShellSchema.ID,
command: Schema.String,
status: ShellSchema.Status,
exit: Schema.Number.pipe(optional),
output: ShellSchema.Output.pipe(optional),
time: Schema.Struct({
created: DateTimeUtcFromMillis,
@ -93,15 +100,15 @@ export const Shell = Schema.Struct({
}),
}).annotate({ identifier: "Session.Message.Shell" })
export interface ToolStatePending extends Schema.Schema.Type<typeof ToolStatePending> {}
export const ToolStatePending = Schema.Struct({
status: Schema.Literal("pending"),
export interface ToolStateStreaming extends Schema.Schema.Type<typeof ToolStateStreaming> {}
export const ToolStateStreaming = Schema.Struct({
status: Schema.tag("streaming"),
input: Schema.String,
}).annotate({ identifier: "Session.Message.ToolState.Pending" })
}).annotate({ identifier: "Session.Message.ToolState.Streaming" })
export interface ToolStateRunning extends Schema.Schema.Type<typeof ToolStateRunning> {}
export const ToolStateRunning = Schema.Struct({
status: Schema.Literal("running"),
status: Schema.tag("running"),
input: Schema.Record(Schema.String, Schema.Unknown),
structured: Schema.Record(Schema.String, Schema.Unknown),
content: ToolContent.pipe(Schema.Array),
@ -109,18 +116,16 @@ export const ToolStateRunning = Schema.Struct({
export interface ToolStateCompleted extends Schema.Schema.Type<typeof ToolStateCompleted> {}
export const ToolStateCompleted = Schema.Struct({
status: Schema.Literal("completed"),
status: Schema.tag("completed"),
input: Schema.Record(Schema.String, Schema.Unknown),
attachments: FileAttachment.pipe(Schema.Array, optional),
content: ToolContent.pipe(Schema.Array),
outputPaths: Schema.Array(Schema.String).pipe(optional),
structured: Schema.Record(Schema.String, Schema.Unknown),
result: Schema.Unknown.pipe(optional),
}).annotate({ identifier: "Session.Message.ToolState.Completed" })
export interface ToolStateError extends Schema.Schema.Type<typeof ToolStateError> {}
export const ToolStateError = Schema.Struct({
status: Schema.Literal("error"),
status: Schema.tag("error"),
input: Schema.Record(Schema.String, Schema.Unknown),
content: ToolContent.pipe(Schema.Array),
structured: Schema.Record(Schema.String, Schema.Unknown),
@ -128,14 +133,14 @@ export const ToolStateError = Schema.Struct({
result: Schema.Unknown.pipe(optional),
}).annotate({ identifier: "Session.Message.ToolState.Error" })
export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe(
export const ToolState = Schema.Union([ToolStateStreaming, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe(
Schema.toTaggedUnion("status"),
)
export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError
export type ToolState = ToolStateStreaming | ToolStateRunning | ToolStateCompleted | ToolStateError
export interface AssistantTool extends Schema.Schema.Type<typeof AssistantTool> {}
export const AssistantTool = Schema.Struct({
type: Schema.Literal("tool"),
type: Schema.tag("tool"),
id: Schema.String,
name: Schema.String,
executed: Schema.Boolean.pipe(optional),
@ -146,19 +151,18 @@ export const AssistantTool = Schema.Struct({
created: DateTimeUtcFromMillis,
ran: DateTimeUtcFromMillis.pipe(optional),
completed: DateTimeUtcFromMillis.pipe(optional),
pruned: DateTimeUtcFromMillis.pipe(optional),
}),
}).annotate({ identifier: "Session.Message.Assistant.Tool" })
export interface AssistantText extends Schema.Schema.Type<typeof AssistantText> {}
export const AssistantText = Schema.Struct({
type: Schema.Literal("text"),
type: Schema.tag("text"),
text: Schema.String,
}).annotate({ identifier: "Session.Message.Assistant.Text" })
export interface AssistantReasoning extends Schema.Schema.Type<typeof AssistantReasoning> {}
export const AssistantReasoning = Schema.Struct({
type: Schema.Literal("reasoning"),
type: Schema.tag("reasoning"),
text: Schema.String,
state: ProviderState.pipe(optional),
time: Schema.Struct({
@ -182,23 +186,18 @@ export const AssistantRetry = Schema.Struct({
export interface Assistant extends Schema.Schema.Type<typeof Assistant> {}
export const Assistant = Schema.Struct({
...Base,
type: Schema.Literal("assistant"),
agent: Schema.String,
type: Schema.tag("assistant"),
agent: Agent.ID,
model: Model.Ref,
content: AssistantContent.pipe(Schema.Array),
snapshot: Schema.Struct({
start: Schema.String.pipe(optional),
end: Schema.String.pipe(optional),
start: Snapshot.ID.pipe(optional),
end: Snapshot.ID.pipe(optional),
files: Schema.Array(RelativePath).pipe(optional),
}).pipe(optional),
finish: FinishReason.pipe(optional),
cost: Schema.Finite.pipe(optional),
tokens: Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({ read: Schema.Finite, write: Schema.Finite }),
}).pipe(optional),
cost: Money.USD.pipe(optional),
tokens: TokenUsage.Info.pipe(optional),
error: SessionError.Error.pipe(optional),
retry: AssistantRetry.pipe(optional),
time: Schema.Struct({
@ -207,17 +206,41 @@ export const Assistant = Schema.Struct({
}),
}).annotate({ identifier: "Session.Message.Assistant" })
export interface Compaction extends Schema.Schema.Type<typeof Compaction> {}
export const Compaction = Schema.Struct({
type: Schema.Literal("compaction"),
status: Schema.Literals(["queued", "running", "completed", "failed"]),
const CompactionBase = { type: Schema.tag("compaction"), ...Base }
export interface CompactionRunning extends Schema.Schema.Type<typeof CompactionRunning> {}
export const CompactionRunning = Schema.Struct({
...CompactionBase,
status: Schema.tag("running"),
reason: Schema.Literals(["auto", "manual"]),
summary: Schema.String,
recent: Schema.String,
...Base,
}).annotate({ identifier: "Session.Message.Compaction" })
}).annotate({ identifier: "Session.Message.Compaction.Running" })
export const Message = Schema.Union([
export interface CompactionCompleted extends Schema.Schema.Type<typeof CompactionCompleted> {}
export const CompactionCompleted = Schema.Struct({
...CompactionBase,
status: Schema.tag("completed"),
reason: Schema.Literals(["auto", "manual"]),
summary: Schema.String,
recent: Schema.String,
}).annotate({ identifier: "Session.Message.Compaction.Completed" })
export interface CompactionFailed extends Schema.Schema.Type<typeof CompactionFailed> {}
export const CompactionFailed = Schema.Struct({
...CompactionBase,
status: Schema.tag("failed"),
reason: Schema.Literals(["auto", "manual"]),
error: SessionError.Error,
}).annotate({ identifier: "Session.Message.Compaction.Failed" })
export const Compaction = Schema.Union([CompactionRunning, CompactionCompleted, CompactionFailed]).pipe(
Schema.toTaggedUnion("status"),
Schema.annotate({ identifier: "Session.Message.Compaction" }),
)
export type Compaction = CompactionRunning | CompactionCompleted | CompactionFailed
export const Info = Schema.Union([
AgentSelected,
ModelSelected,
User,
@ -229,6 +252,6 @@ export const Message = Schema.Union([
Compaction,
])
.pipe(Schema.toTaggedUnion("type"))
.annotate({ identifier: "Session.Message" })
export type Message = AgentSelected | ModelSelected | User | Synthetic | System | Skill | Shell | Assistant | Compaction
export type Type = Message["type"]
.annotate({ identifier: "Session.Message.Info" })
export type Info = AgentSelected | ModelSelected | User | Synthetic | System | Skill | Shell | Assistant | Compaction
export type Type = Info["type"]

View file

@ -0,0 +1,86 @@
import { Schema, SchemaTransformation } from "effect"
import { FileDiff } from "./file-diff.js"
import { optional } from "./schema.js"
import { SessionMessage } from "./session-message.js"
import { Snapshot } from "./snapshot.js"
export interface Revert extends Schema.Schema.Type<typeof Revert> {}
export const Revert = Schema.Struct({
messageID: SessionMessage.ID,
/** Legacy V1 compatibility state. */
partID: Schema.String.pipe(optional),
snapshot: Snapshot.ID.pipe(optional),
files: Schema.Array(FileDiff.Info).pipe(optional),
}).annotate({ identifier: "Session.Revert" })
const FileDiffV1 = Schema.Struct({
path: Schema.String,
status: Schema.Literals(["added", "modified", "deleted"]),
additions: Schema.Finite,
deletions: Schema.Finite,
patch: Schema.String,
})
export interface RevertV1 extends Schema.Schema.Type<typeof RevertV1> {}
export const RevertV1 = Schema.Struct({
messageID: SessionMessage.ID,
partID: Schema.String.pipe(optional),
snapshot: Schema.String.pipe(optional),
diff: Schema.String.pipe(optional),
files: Schema.Array(FileDiffV1).pipe(optional),
}).annotate({ identifier: "Session.RevertV1" })
const PersistedCurrent = Revert.pipe(
Schema.decodeTo(
Schema.Struct({ source: Schema.tag("current"), revert: Schema.toType(Revert) }),
SchemaTransformation.transform({
decode: (revert): { readonly source: "current"; readonly revert: Revert } => ({
source: "current",
revert,
}),
encode: (value) => value.revert,
}),
),
)
const PersistedLegacy = RevertV1.pipe(
Schema.decodeTo(
Schema.Struct({ source: Schema.tag("legacy"), revert: Schema.toType(RevertV1) }),
SchemaTransformation.transform({
decode: (revert): { readonly source: "legacy"; readonly revert: RevertV1 } => ({
source: "legacy",
revert,
}),
encode: (value) => value.revert,
}),
),
)
/** Storage decoder for revert state written before FileDiff became canonical. */
export const PersistedRevert = Schema.Union([PersistedCurrent, PersistedLegacy]).pipe(
Schema.toTaggedUnion("source"),
Schema.decodeTo(
Schema.toType(Revert),
SchemaTransformation.transform({
decode: (persisted): Revert => {
if (persisted.source === "current") return persisted.revert
return Revert.make({
messageID: persisted.revert.messageID,
partID: persisted.revert.partID,
snapshot: persisted.revert.snapshot ? Snapshot.ID.make(persisted.revert.snapshot) : undefined,
files: persisted.revert.files?.map((file) => ({
file: file.path,
status: file.status,
additions: file.additions,
deletions: file.deletions,
patch: file.patch,
})),
})
},
encode: (revert): { readonly source: "current"; readonly revert: Revert } => ({
source: "current",
revert,
}),
}),
),
Schema.annotate({ identifier: "Session.Revert.Persisted" }),
)

View file

@ -9,34 +9,31 @@ import { DateTimeUtcFromMillis, optional, RelativePath } from "./schema.js"
import { SessionEvent } from "./session-event.js"
import { SessionID } from "./session-id.js"
import { SessionMessage } from "./session-message.js"
import { Revert } from "./revert.js"
import { Money } from "./money.js"
import { TokenUsage } from "./token-usage.js"
import { Revert } from "./session-revert.js"
export const ID = SessionID
export type ID = SessionID
export const Event = SessionEvent
export { Revert }
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({
id: ID,
parentID: ID.pipe(optional),
fork: Schema.Struct({
sessionID: ID,
/** Messages before this exclusive boundary are copied into the fork. */
messageID: SessionMessage.ID.pipe(optional),
}).pipe(optional),
projectID: Project.ID,
agent: Agent.ID.pipe(optional),
model: Model.Ref.pipe(optional),
cost: Schema.Finite,
tokens: Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
cost: Money.USD,
tokens: TokenUsage.Info,
time: Schema.Struct({
created: DateTimeUtcFromMillis,
updated: DateTimeUtcFromMillis,
@ -45,8 +42,8 @@ export const Info = Schema.Struct({
title: Schema.String,
location: Location.Ref,
subpath: RelativePath.pipe(optional),
revert: Revert.State.pipe(optional),
}).annotate({ identifier: "SessionV2.Info" })
revert: Revert.pipe(optional),
}).annotate({ identifier: "Session.Info" })
export const ListAnchor = Schema.Struct({
id: ID,

View file

@ -6,7 +6,7 @@ import { ephemeral, inventory } from "./event.js"
import { ascending } from "./identifier.js"
import { NonNegativeInt, statics } from "./schema.js"
const IDSchema = Schema.String.check(Schema.isStartsWith("sh_")).pipe(Schema.brand("ShellID"))
const IDSchema = Schema.String.check(Schema.isStartsWith("sh_")).pipe(Schema.brand("Shell.ID"))
export const ID = IDSchema.pipe(
statics((schema: typeof IDSchema) => {

View file

@ -5,49 +5,56 @@ import { optional } from "./schema.js"
import { AbsolutePath } from "./schema.js"
import { ephemeral, inventory } from "./event.js"
export const ID = Schema.String.pipe(Schema.brand("Skill.ID"))
export type ID = typeof ID.Type
export const Name = Schema.String.pipe(Schema.brand("Skill.Name"))
export type Name = typeof Name.Type
export interface DirectorySource extends Schema.Schema.Type<typeof DirectorySource> {}
export const DirectorySource = Schema.Struct({
type: Schema.Literal("directory"),
type: Schema.tag("directory"),
path: AbsolutePath,
}).annotate({ identifier: "SkillV2.DirectorySource" })
}).annotate({ identifier: "Skill.DirectorySource" })
export interface UrlSource extends Schema.Schema.Type<typeof UrlSource> {}
export const UrlSource = Schema.Struct({
type: Schema.Literal("url"),
type: Schema.tag("url"),
url: Schema.String,
}).annotate({ identifier: "SkillV2.UrlSource" })
}).annotate({ identifier: "Skill.UrlSource" })
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({
name: Schema.String,
id: ID,
name: Name,
description: Schema.String.pipe(optional),
slash: Schema.Boolean.pipe(optional),
autoinvoke: Schema.Boolean.pipe(optional),
location: AbsolutePath,
content: Schema.String,
}).annotate({ identifier: "SkillV2.Info" })
}).annotate({ identifier: "Skill.Info" })
const Updated = ephemeral({ type: "skill.updated", schema: {} })
export const Event = { Updated, Definitions: inventory(Updated) }
export interface EmbeddedSource extends Schema.Schema.Type<typeof EmbeddedSource> {}
export const EmbeddedSource = Schema.Struct({
type: Schema.Literal("embedded"),
type: Schema.tag("embedded"),
skill: Schema.suspend(() => Info),
}).annotate({ identifier: "SkillV2.EmbeddedSource" })
}).annotate({ identifier: "Skill.EmbeddedSource" })
export type Source = DirectorySource | UrlSource | EmbeddedSource
export const Source = Object.assign(
Schema.Union([DirectorySource, UrlSource, EmbeddedSource]).pipe(
Schema.toTaggedUnion("type"),
Schema.annotate({ identifier: "SkillV2.Source" }),
Schema.annotate({ identifier: "Skill.Source" }),
),
{
equals: (a: Source, b: Source) => {
if (a.type !== b.type) return false
if (a.type === "directory" && b.type === "directory") return a.path === b.path
if (a.type === "url" && b.type === "url") return a.url === b.url
if (a.type === "embedded" && b.type === "embedded") return a.skill.name === b.skill.name
if (a.type === "embedded" && b.type === "embedded") return a.skill.id === b.skill.id
return false
},
key: (source: Source) =>
@ -55,6 +62,6 @@ export const Source = Object.assign(
? `directory:${source.path}`
: source.type === "url"
? `url:${source.url}`
: `embedded:${source.skill.name}`,
: `embedded:${source.skill.id}`,
},
)

View file

@ -0,0 +1,6 @@
export * as Snapshot from "./snapshot.js"
import { Schema } from "effect"
export const ID = Schema.String.pipe(Schema.brand("Snapshot.ID"))
export type ID = typeof ID.Type

View file

@ -0,0 +1,14 @@
export * as TokenUsage from "./token-usage.js"
import { Schema } from "effect"
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}).annotate({ identifier: "TokenUsage.Info" })

View file

@ -2,7 +2,6 @@ export * as SessionV1 from "./session.js"
import { Effect, Schema, Types } from "effect"
import { durable, ephemeral, inventory } from "../event.js"
import { FileDiff } from "../file-diff.js"
import { Project } from "../project.js"
import { Provider } from "../provider.js"
import { Model } from "../model.js"
@ -11,6 +10,7 @@ import { ascending } from "../identifier.js"
import { SessionID } from "../session-id.js"
import { WorkspaceID } from "../workspace-id.js"
import { PermissionV1 } from "./permission.js"
import { FileDiff } from "../file-diff.js"
const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0))
@ -340,7 +340,7 @@ export const User = Schema.Struct({
Schema.Struct({
title: Schema.optional(Schema.String),
body: Schema.optional(Schema.String),
diffs: Schema.Array(FileDiff.Info),
diffs: Schema.Array(FileDiff.LegacyInfo),
}),
),
agent: Schema.String,
@ -510,7 +510,7 @@ const SessionSummary = Schema.Struct({
additions: Schema.Finite,
deletions: Schema.Finite,
files: Schema.Finite,
diffs: optional(Schema.Array(FileDiff.Info)),
diffs: optional(Schema.Array(FileDiff.LegacyInfo)),
})
const SessionTokens = Schema.Struct({
@ -565,7 +565,7 @@ export const SessionInfo = Schema.Struct({
}),
permission: optional(PermissionV1.Ruleset),
revert: optional(SessionRevert),
}).annotate({ identifier: "Session" })
}).annotate({ identifier: "SessionV1.Info" })
export type SessionInfo = typeof SessionInfo.Type
const events = {
@ -644,7 +644,7 @@ export const Diff = ephemeral({
type: "session.diff",
schema: {
sessionID: SessionID,
diff: Schema.Array(FileDiff.Info),
diff: Schema.Array(FileDiff.LegacyInfo),
},
})

View file

@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import { DateTime, Schema } from "effect"
import { Agent } from "../src/agent.js"
import { FileSystem } from "../src/filesystem.js"
import { Mcp } from "../src/mcp.js"
import { Model } from "../src/model.js"
import { Project } from "../src/project.js"
import { Provider } from "../src/provider.js"
@ -9,10 +10,30 @@ import { Pty } from "../src/pty.js"
import { Question } from "../src/question.js"
import { Session } from "../src/session.js"
import { SessionMessage } from "../src/session-message.js"
import { SessionInput } from "../src/session-input.js"
import { FileDiff } from "../src/file-diff.js"
import { Money } from "../src/money.js"
import { Skill } from "../src/skill.js"
import { Shell } from "../src/shell.js"
import { PersistedRevert } from "../src/session-revert.js"
import { SessionTodo } from "../src/session-todo.js"
import { optional } from "../src/schema.js"
describe("contract hygiene", () => {
test("keeps absolute costs distinct from model rates", () => {
const usd = Money.USD.make(1)
const rate = Money.USDPerMillionTokens.make(1)
// @ts-expect-error Model rates are not absolute costs.
const invalidUSD: Money.USD = rate
// @ts-expect-error Absolute costs are not model rates.
const invalidRate: Money.USDPerMillionTokens = usd
expect(invalidUSD).toBe(Money.USD.make(1))
expect(invalidRate).toBe(Money.USDPerMillionTokens.make(1))
expect(Money.USD.zero).toBe(Money.USD.make(0))
expect(Money.USDPerMillionTokens.zero).toBe(Money.USDPerMillionTokens.make(0))
})
test("optional properties preserve transformations and omit undefined while encoding", () => {
const Value = Schema.Struct({ value: optional(Schema.FiniteFromString) })
expect(Schema.decodeUnknownSync(Value)({ value: "1" })).toEqual({ value: 1 })
@ -51,6 +72,11 @@ describe("contract hygiene", () => {
const identifiers = [
Agent.Color,
FileSystem.Submatch,
Mcp.Resource,
Mcp.ResourceTemplate,
Mcp.ResourceCatalog,
Mcp.ResourceContentPart,
Mcp.ResourceContent,
Model.Ref,
Model.Capabilities,
Model.Cost,
@ -65,6 +91,7 @@ describe("contract hygiene", () => {
Project.Info,
Pty.Info,
Session.ListAnchor,
Session.Revert,
].map((schema) => schema.ast.annotations?.identifier)
expect(identifiers.every((identifier) => typeof identifier === "string")).toBe(true)
@ -98,9 +125,76 @@ describe("contract hygiene", () => {
name: "search",
executed: true,
providerState: { itemId: "item_1" },
state: { status: "pending", input: "" },
state: { status: "streaming", input: "" },
time: { created: DateTime.makeUnsafe(0) },
}),
).not.toHaveProperty("provider")
})
test("reviewed session contracts use their canonical current shapes", () => {
expect(SessionMessage.Info.ast.annotations?.identifier).toBe("Session.Message.Info")
expect(SessionInput.Info.ast.annotations?.identifier).toBe("SessionInput.Info")
expect(Money.USD).not.toBe(Money.USDPerMillionTokens)
expect(
FileDiff.Info.make({ file: "src/index.ts", patch: "@@", additions: 1, deletions: 0, status: "modified" }),
).toEqual({ file: "src/index.ts", patch: "@@", additions: 1, deletions: 0, status: "modified" })
expect(
SessionMessage.Shell.make({
id: SessionMessage.ID.make("msg_shell"),
type: "shell",
shellID: Shell.ID.make("sh_test"),
command: "pwd",
status: "exited",
exit: 0,
time: { created: DateTime.makeUnsafe(0) },
}),
).not.toHaveProperty("shell")
expect(
SessionMessage.Skill.make({
id: SessionMessage.ID.make("msg_skill"),
type: "skill",
skill: Skill.ID.make("effect"),
name: Skill.Name.make("Effect"),
text: "Use Effect",
time: { created: DateTime.makeUnsafe(0) },
}),
).toMatchObject({ skill: "effect", name: "Effect" })
expect(
SessionMessage.CompactionFailed.make({
id: SessionMessage.ID.make("msg_compaction"),
type: "compaction",
status: "failed",
reason: "manual",
error: { type: "compaction.failed", message: "failed" },
time: { created: DateTime.makeUnsafe(0) },
}),
).not.toHaveProperty("summary")
})
test("keeps shared persisted revert compatibility", () => {
expect(
Schema.decodeUnknownSync(Session.Revert)({
messageID: "msg_legacy",
snapshot: "tree",
diff: "legacy patch",
}),
).not.toHaveProperty("diff")
const revert = Schema.decodeUnknownSync(PersistedRevert)({
messageID: "msg_legacy",
snapshot: "tree",
diff: "legacy patch",
files: [{ path: "src/index.ts", status: "modified", additions: 1, deletions: 0, patch: "@@" }],
})
expect(String(revert.messageID)).toBe("msg_legacy")
expect(String(revert.snapshot)).toBe("tree")
expect(revert.files).toEqual([
{ file: "src/index.ts", status: "modified", additions: 1, deletions: 0, patch: "@@" },
])
expect(Schema.encodeSync(PersistedRevert)(revert)).toEqual({
messageID: "msg_legacy",
snapshot: "tree",
files: [{ file: "src/index.ts", status: "modified", additions: 1, deletions: 0, patch: "@@" }],
})
})
})

View file

@ -51,6 +51,7 @@ describe("public event manifest", () => {
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
expect(EventManifest.Latest.get("plugin.updated")).toBe(Plugin.Event.Updated)
expect(EventManifest.Server.get("mcp.status.changed")).toBe(McpEvent.StatusChanged)
expect(EventManifest.Server.get("mcp.resources.changed")).toBe(McpEvent.ResourcesChanged)
expect(EventManifest.Server.get("session.deleted")).toBe(SessionEvent.Deleted)
expect(EventManifest.Server.has("mcp.tools.changed")).toBe(false)
expect(Agent.Event.Updated.durable).toBeUndefined()
@ -76,7 +77,7 @@ describe("public event manifest", () => {
expect(Form.Event.Definitions).toEqual([Form.Event.Created, Form.Event.Replied, Form.Event.Cancelled])
expect(Reference.Event.Definitions).toEqual([Reference.Event.Updated])
expect(Plugin.Event.Definitions).toEqual([Plugin.Event.Added, Plugin.Event.Updated])
expect(McpEvent.Definitions).toEqual([McpEvent.ToolsChanged, McpEvent.StatusChanged])
expect(McpEvent.Definitions).toEqual([McpEvent.ToolsChanged, McpEvent.ResourcesChanged, McpEvent.StatusChanged])
expect(EventManifest.Latest.has("mcp.browser.open.failed")).toBe(false)
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
expect(IdeEvent.Definitions).toEqual([IdeEvent.Installed])
@ -143,9 +144,18 @@ describe("public event manifest", () => {
expect(SessionEvent.DurableDefinitions).toEqual(
SessionEvent.Definitions.filter((definition) => definition.durability === "durable"),
)
expect(SessionEvent.UsageUpdated.durability).toBe("ephemeral")
expect(SessionEvent.Compaction.Delta.durability).toBe("ephemeral")
expect(EventManifest.Durable.has("session.compaction.delta.1")).toBe(false)
expect(EventManifest.ServerDefinitions).toContain(SessionEvent.UsageUpdated)
expect(EventManifest.Definitions.every((definition) => definition.durability !== undefined)).toBe(true)
})
test("uses the current Session skill event as durable version 1", () => {
expect(EventManifest.Durable.get("session.skill.activated.1")).toBe(SessionEvent.Skill.Activated)
expect(EventManifest.Latest.get("session.skill.activated")).toBe(SessionEvent.Skill.Activated)
})
test("keeps simplified session fragment and tool payloads on durable version 1", () => {
const sessionID = SessionID.make("ses_test")
const assistantMessageID = SessionMessage.ID.make("msg_test")

View file

@ -0,0 +1,37 @@
import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { Mcp } from "../src/mcp.js"
describe("Mcp resources", () => {
test("decodes resource catalogs and omits absent metadata", () => {
const value = Schema.decodeUnknownSync(Mcp.ResourceCatalog)({
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
})
expect(Schema.encodeSync(Mcp.ResourceCatalog)(value)).toEqual({
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
})
})
test("preserves text and base64 blob contents", () => {
expect(
Schema.decodeUnknownSync(Mcp.ResourceContent)({
server: "docs",
uri: "docs://readme",
contents: [
{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" },
{ type: "blob", uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
],
}),
).toEqual({
server: "docs",
uri: "docs://readme",
contents: [
{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" },
{ type: "blob", uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
],
})
})
})