mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-08 18:06:25 +00:00
Compare commits
2
Commits
v2
..
plugin-fork
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57843b0979 | ||
|
|
8aec1aba21 |
@@ -518,6 +518,7 @@ export type SessionLogOutput =
|
||||
readonly sessionID: Session.ID
|
||||
readonly parentID: Session.ID
|
||||
readonly boundary: Session.ForkBoundary
|
||||
readonly messages?: ReadonlyArray<SessionMessage.InfoEncoded> | undefined
|
||||
readonly instructions?:
|
||||
| { readonly [x: string & Brand.Brand<"Instruction.Key">]: string & Brand.Brand<"Instruction.Hash"> }
|
||||
| undefined
|
||||
@@ -714,7 +715,7 @@ export type SessionLogOutput =
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
|
||||
readonly rawFinish?: string | undefined
|
||||
readonly providerState?: SessionMessage.ProviderState | undefined
|
||||
readonly providerState?: { readonly [x: string]: unknown } | undefined
|
||||
readonly cost: number & Brand.Brand<"Money.USD">
|
||||
readonly tokens: {
|
||||
readonly input: number
|
||||
@@ -739,7 +740,7 @@ export type SessionLogOutput =
|
||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||
readonly finish?: "content-filter" | undefined
|
||||
readonly rawFinish?: string | undefined
|
||||
readonly providerState?: SessionMessage.ProviderState | undefined
|
||||
readonly providerState?: { readonly [x: string]: unknown } | undefined
|
||||
readonly cost?: (number & Brand.Brand<"Money.USD">) | undefined
|
||||
readonly tokens?:
|
||||
| {
|
||||
@@ -778,7 +779,7 @@ export type SessionLogOutput =
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly ordinal: number
|
||||
readonly text: string
|
||||
readonly state?: SessionMessage.ProviderState | undefined
|
||||
readonly state?: { readonly [x: string]: unknown } | undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
@@ -792,7 +793,7 @@ export type SessionLogOutput =
|
||||
readonly sessionID: Session.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly ordinal: number
|
||||
readonly state?: SessionMessage.ProviderState | undefined
|
||||
readonly state?: { readonly [x: string]: unknown } | undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
@@ -807,7 +808,7 @@ export type SessionLogOutput =
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly ordinal: number
|
||||
readonly text: string
|
||||
readonly state?: SessionMessage.ProviderState | undefined
|
||||
readonly state?: { readonly [x: string]: unknown } | undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
@@ -851,7 +852,7 @@ export type SessionLogOutput =
|
||||
readonly id: string
|
||||
readonly input: { readonly [x: string]: unknown }
|
||||
readonly executed: boolean
|
||||
readonly state?: SessionMessage.ProviderState | undefined
|
||||
readonly state?: { readonly [x: string]: unknown } | undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
@@ -887,7 +888,7 @@ export type SessionLogOutput =
|
||||
]
|
||||
readonly metadata?: { readonly [x: string]: Schema.Json } | undefined
|
||||
readonly executed: boolean
|
||||
readonly resultState?: SessionMessage.ProviderState | undefined
|
||||
readonly resultState?: { readonly [x: string]: unknown } | undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
@@ -926,7 +927,7 @@ export type SessionLogOutput =
|
||||
| undefined
|
||||
readonly metadata?: { readonly [x: string]: Schema.Json } | undefined
|
||||
readonly executed: boolean
|
||||
readonly resultState?: SessionMessage.ProviderState | undefined
|
||||
readonly resultState?: { readonly [x: string]: unknown } | undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
@@ -969,7 +970,7 @@ export type SessionLogOutput =
|
||||
readonly sessionID: Session.ID
|
||||
readonly reason: "auto" | "manual"
|
||||
readonly model?: Model.Ref | undefined
|
||||
readonly providerState?: SessionMessage.ProviderState | undefined
|
||||
readonly providerState?: { readonly [x: string]: unknown } | undefined
|
||||
readonly providerContext?:
|
||||
| {
|
||||
readonly version: 1
|
||||
|
||||
@@ -159,6 +159,76 @@ export type InstructionEntryKey = string
|
||||
|
||||
export type SessionGenerateResponse = { data: { text: string } }
|
||||
|
||||
export type SessionMessageAgentSelected1 = {
|
||||
id: string
|
||||
metadata?: { [x: string]: any }
|
||||
time: { created: number }
|
||||
type: "agent-switched"
|
||||
agent: string
|
||||
previous?: string
|
||||
}
|
||||
|
||||
export type SessionMessageSynthetic1 = {
|
||||
id: string
|
||||
metadata?: { [x: string]: any }
|
||||
time: { created: number }
|
||||
text: string
|
||||
description?: string
|
||||
type: "synthetic"
|
||||
}
|
||||
|
||||
export type SessionMessageSystem1 = {
|
||||
id: string
|
||||
metadata?: { [x: string]: any }
|
||||
time: { created: number }
|
||||
type: "system"
|
||||
text: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type SessionMessageSkill1 = {
|
||||
id: string
|
||||
metadata?: { [x: string]: any }
|
||||
time: { created: number }
|
||||
type: "skill"
|
||||
skill: string
|
||||
name: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export type SessionMessageShell1 = {
|
||||
id: string
|
||||
metadata?: { [x: string]: any }
|
||||
time: { created: number; completed?: number }
|
||||
type: "shell"
|
||||
shellID: string
|
||||
command: string
|
||||
status: "running" | "exited" | "timeout" | "killed"
|
||||
exit?: number
|
||||
output?: { output: string; cursor: number; size: number; truncated: boolean }
|
||||
}
|
||||
|
||||
export type SessionMessageProviderState1 = { [x: string]: any }
|
||||
|
||||
export type SessionMessageToolStateRunning1 = {
|
||||
status: "running"
|
||||
input: { [x: string]: any }
|
||||
metadata: { [x: string]: JsonValue }
|
||||
}
|
||||
|
||||
export type ToolFileContent1 = { type: "file"; uri: string; mime: string; name?: string | undefined }
|
||||
|
||||
export type SessionMessageCompactionRunning1 = {
|
||||
type: "compaction"
|
||||
id: string
|
||||
metadata?: { [x: string]: any }
|
||||
time: { created: number }
|
||||
status: "running"
|
||||
reason: "auto" | "manual"
|
||||
summary: string
|
||||
recent: string
|
||||
}
|
||||
|
||||
export type SessionInboxSyntheticPayload1 = { text: string; description?: string; metadata?: { [x: string]: any } }
|
||||
|
||||
export type ShellInfo = {
|
||||
@@ -174,16 +244,6 @@ export type ShellInfo = {
|
||||
time: { started: number; completed?: number }
|
||||
}
|
||||
|
||||
export type SessionMessageProviderState1 = { [x: string]: any }
|
||||
|
||||
export type ToolFileContent1 = { type: "file"; uri: string; mime: string; name?: string | undefined }
|
||||
|
||||
export type SessionMessageToolStateRunning1 = {
|
||||
status: "running"
|
||||
input: { [x: string]: any }
|
||||
metadata: { [x: string]: JsonValue }
|
||||
}
|
||||
|
||||
export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: number }
|
||||
|
||||
export type SessionInterruptResponse = { interrupted: boolean }
|
||||
@@ -454,6 +514,17 @@ export type SessionMessageLocationSwitched = {
|
||||
|
||||
export type SessionInboxMovePayload = { location: LocationRef; projectID: string; subpath?: string }
|
||||
|
||||
export type SessionMessageLocationSwitched1 = {
|
||||
id: string
|
||||
metadata?: { [x: string]: any }
|
||||
time: { created: number }
|
||||
type: "location-switched"
|
||||
location: LocationRef
|
||||
projectID?: string
|
||||
subpath?: string
|
||||
previous?: { location: LocationRef; projectID?: string; subpath?: string }
|
||||
}
|
||||
|
||||
export type V2EventRpc = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -489,6 +560,15 @@ export type SessionMessageModelSelected = {
|
||||
previous?: ModelRef
|
||||
}
|
||||
|
||||
export type SessionMessageModelSelected1 = {
|
||||
id: string
|
||||
metadata?: { [x: string]: any }
|
||||
time: { created: number }
|
||||
type: "model-switched"
|
||||
model: ModelRef
|
||||
previous?: ModelRef
|
||||
}
|
||||
|
||||
export type PromptFileAttachment = {
|
||||
data: PromptBase64
|
||||
mime: string
|
||||
@@ -525,6 +605,16 @@ export type SessionMessageCompactionFailed = {
|
||||
error: SessionStructuredError
|
||||
}
|
||||
|
||||
export type SessionMessageCompactionFailed1 = {
|
||||
type: "compaction"
|
||||
id: string
|
||||
metadata?: { [x: string]: any }
|
||||
time: { created: number }
|
||||
status: "failed"
|
||||
reason: "auto" | "manual"
|
||||
error: SessionStructuredError
|
||||
}
|
||||
|
||||
export type SessionProviderContext = { version: 1; provenance: SessionProviderContextProvenance; messages: JsonValue }
|
||||
|
||||
export type SessionInboxSynthetic = {
|
||||
@@ -1200,37 +1290,13 @@ export type McpResourcesChanged = {
|
||||
data: { server: string }
|
||||
}
|
||||
|
||||
export type SessionShellStarted = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.shell.started"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; shell: ShellInfo }
|
||||
}
|
||||
export type SessionMessageAssistantText1 = { type: "text"; text: string; state?: SessionMessageProviderState1 }
|
||||
|
||||
export type SessionShellEnded = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.shell.ended"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: {
|
||||
sessionID: string
|
||||
shell: ShellInfo
|
||||
output: { output: string; cursor: number; size: number; truncated: boolean }
|
||||
}
|
||||
}
|
||||
|
||||
export type ShellCreated = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "shell.created"
|
||||
location?: LocationRef
|
||||
data: { info: ShellInfo }
|
||||
export type SessionMessageAssistantReasoning1 = {
|
||||
type: "reasoning"
|
||||
text: string
|
||||
state?: SessionMessageProviderState1
|
||||
time?: { created: number; completed?: number }
|
||||
}
|
||||
|
||||
export type SessionStepEnded = {
|
||||
@@ -1333,16 +1399,40 @@ export type SessionToolCalled = {
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionMessageAssistantText1 = { type: "text"; text: string; state?: SessionMessageProviderState1 }
|
||||
export type ToolContent1 = ToolTextContent | ToolFileContent1
|
||||
|
||||
export type SessionMessageAssistantReasoning1 = {
|
||||
type: "reasoning"
|
||||
text: string
|
||||
state?: SessionMessageProviderState1
|
||||
time?: { created: number; completed?: number }
|
||||
export type SessionShellStarted = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.shell.started"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; shell: ShellInfo }
|
||||
}
|
||||
|
||||
export type ToolContent1 = ToolTextContent | ToolFileContent1
|
||||
export type SessionShellEnded = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.shell.ended"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: {
|
||||
sessionID: string
|
||||
shell: ShellInfo
|
||||
output: { output: string; cursor: number; size: number; truncated: boolean }
|
||||
}
|
||||
}
|
||||
|
||||
export type ShellCreated = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "shell.created"
|
||||
location?: LocationRef
|
||||
data: { info: ShellInfo }
|
||||
}
|
||||
|
||||
export type ModelCompatibility = {
|
||||
reasoningField?: ModelReasoningField
|
||||
@@ -1703,6 +1793,17 @@ export type SessionInboxUserPayload = {
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
}
|
||||
|
||||
export type SessionMessageUser1 = {
|
||||
id: string
|
||||
metadata?: { [x: string]: any }
|
||||
time: { created: number }
|
||||
text: string
|
||||
files?: Array<PromptFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
skills?: Array<PromptSkillAttachment>
|
||||
type: "user"
|
||||
}
|
||||
|
||||
export type SessionInboxUserPayload1 = {
|
||||
text: string
|
||||
files?: Array<PromptFileAttachment>
|
||||
@@ -1740,6 +1841,20 @@ export type SessionMessageCompactionCompleted = {
|
||||
providerContext?: SessionProviderContext
|
||||
}
|
||||
|
||||
export type SessionMessageCompactionCompleted1 = {
|
||||
type: "compaction"
|
||||
id: string
|
||||
metadata?: { [x: string]: any }
|
||||
time: { created: number }
|
||||
status: "completed"
|
||||
reason: "auto" | "manual"
|
||||
model?: ModelRef
|
||||
providerState?: SessionMessageProviderState1
|
||||
summary: string
|
||||
recent: string
|
||||
providerContext?: SessionProviderContext
|
||||
}
|
||||
|
||||
export type SessionCompactionEnded = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -1758,20 +1873,19 @@ export type SessionCompactionEnded = {
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionForked = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.forked"
|
||||
durable: { aggregateID: string; seq: number; version: 2 }
|
||||
location?: LocationRef
|
||||
data: {
|
||||
sessionID: string
|
||||
parentID: string
|
||||
boundary: SessionForkBoundary
|
||||
instructions?: { [x: string]: string }
|
||||
instructionEntries?: InstructionEntrySnapshot
|
||||
}
|
||||
export type SessionMessageToolStateCompleted1 = {
|
||||
status: "completed"
|
||||
input: { [x: string]: any }
|
||||
content: [ToolContent1, ...Array<ToolContent1>]
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
}
|
||||
|
||||
export type SessionMessageToolStateError1 = {
|
||||
status: "error"
|
||||
input: { [x: string]: any }
|
||||
error: SessionStructuredError
|
||||
content?: [ToolContent1, ...Array<ToolContent1>]
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
}
|
||||
|
||||
export type SessionToolSuccess = {
|
||||
@@ -1811,21 +1925,6 @@ export type SessionToolFailed = {
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionMessageToolStateCompleted1 = {
|
||||
status: "completed"
|
||||
input: { [x: string]: any }
|
||||
content: [ToolContent1, ...Array<ToolContent1>]
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
}
|
||||
|
||||
export type SessionMessageToolStateError1 = {
|
||||
status: "error"
|
||||
input: { [x: string]: any }
|
||||
error: SessionStructuredError
|
||||
content?: [ToolContent1, ...Array<ToolContent1>]
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
}
|
||||
|
||||
export type ModelInfo = {
|
||||
id: string
|
||||
modelID: string
|
||||
@@ -2104,6 +2203,11 @@ export type SessionMessageCompaction =
|
||||
| SessionMessageCompactionCompleted
|
||||
| SessionMessageCompactionFailed
|
||||
|
||||
export type SessionMessageCompaction1 =
|
||||
| SessionMessageCompactionRunning1
|
||||
| SessionMessageCompactionCompleted1
|
||||
| SessionMessageCompactionFailed1
|
||||
|
||||
export type SessionMessageAssistantTool1 = {
|
||||
type: "tool"
|
||||
id: string
|
||||
@@ -2153,6 +2257,24 @@ export type SessionMessageAssistant = {
|
||||
retry?: SessionMessageAssistantRetry
|
||||
}
|
||||
|
||||
export type SessionMessageAssistant1 = {
|
||||
id: string
|
||||
metadata?: { [x: string]: any }
|
||||
time: { created: number; streamed?: number; completed?: number }
|
||||
type: "assistant"
|
||||
agent: string
|
||||
model: ModelRef
|
||||
content: Array<SessionMessageAssistantText1 | SessionMessageAssistantReasoning1 | SessionMessageAssistantTool1>
|
||||
snapshot?: { start?: string; end?: string; files?: Array<string> }
|
||||
finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
|
||||
rawFinish?: string
|
||||
providerState?: SessionMessageProviderState1
|
||||
cost?: MoneyUSD
|
||||
tokens?: TokenUsageInfo
|
||||
error?: SessionStructuredError
|
||||
retry?: SessionMessageAssistantRetry
|
||||
}
|
||||
|
||||
export type SessionMessageAssistantContentEncoded =
|
||||
| SessionMessageAssistantText1
|
||||
| SessionMessageAssistantReasoning1
|
||||
@@ -2178,6 +2300,18 @@ export type SessionMessageInfo =
|
||||
| SessionMessageAssistant
|
||||
| SessionMessageCompaction
|
||||
|
||||
export type SessionMessageInfoEncoded =
|
||||
| SessionMessageAgentSelected1
|
||||
| SessionMessageModelSelected1
|
||||
| SessionMessageLocationSwitched1
|
||||
| SessionMessageUser1
|
||||
| SessionMessageSynthetic1
|
||||
| SessionMessageSystem1
|
||||
| SessionMessageSkill1
|
||||
| SessionMessageShell1
|
||||
| SessionMessageAssistant1
|
||||
| SessionMessageCompaction1
|
||||
|
||||
export type SessionMessageContentUpdated = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -2210,6 +2344,31 @@ export type SessionMessagesResponse = {
|
||||
cursor: { previous?: string | null; next?: string | null }
|
||||
}
|
||||
|
||||
export type SessionForked = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.forked"
|
||||
durable: { aggregateID: string; seq: number; version: 2 }
|
||||
location?: LocationRef
|
||||
data: {
|
||||
sessionID: string
|
||||
parentID: string
|
||||
boundary: SessionForkBoundary
|
||||
messages?: Array<SessionMessageInfoEncoded>
|
||||
instructions?: { [x: string]: string }
|
||||
instructionEntries?: InstructionEntrySnapshot
|
||||
}
|
||||
}
|
||||
|
||||
export type IntegrationInfo = {
|
||||
id: string
|
||||
name: string
|
||||
metadata?: { [x: string]: any }
|
||||
methods: Array<IntegrationMethod>
|
||||
connections: Array<ConnectionInfo>
|
||||
}
|
||||
|
||||
export type SessionEventDurable =
|
||||
| SessionCreated
|
||||
| SessionAgentSelected
|
||||
@@ -2255,14 +2414,6 @@ export type SessionEventDurable =
|
||||
| SessionMessageContentUpdated
|
||||
| SessionUsageRecorded
|
||||
|
||||
export type IntegrationInfo = {
|
||||
id: string
|
||||
name: string
|
||||
metadata?: { [x: string]: any }
|
||||
methods: Array<IntegrationMethod>
|
||||
connections: Array<ConnectionInfo>
|
||||
}
|
||||
|
||||
export type V2Event =
|
||||
| ModelsDevRefreshed
|
||||
| CredentialUpdated
|
||||
|
||||
@@ -513,6 +513,7 @@ export const make = Effect.fn("PluginHost.make")(function* (
|
||||
input?.location ?? Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }),
|
||||
}),
|
||||
get: (input) => sessions.get(input.sessionID),
|
||||
fork: sessions.fork,
|
||||
switchAgent: sessions.switchAgent,
|
||||
switchModel: sessions.switchModel,
|
||||
prompt: sessions.prompt,
|
||||
|
||||
@@ -4,7 +4,7 @@ export * from "./session/schema.js"
|
||||
import { Effect, Layer, Schema, Context, Stream } from "effect"
|
||||
import { LLMClient } from "@opencode/ai"
|
||||
import { ListAnchor } from "@opencode/schema/session"
|
||||
import { and, desc, eq } from "drizzle-orm"
|
||||
import { and, asc, desc, eq, lt, lte, sql } from "drizzle-orm"
|
||||
import { Project } from "./project.js"
|
||||
import { Model } from "@opencode/schema/model"
|
||||
import { Location } from "./location.js"
|
||||
@@ -23,6 +23,7 @@ import { Slug } from "./util/slug.js"
|
||||
import path from "path"
|
||||
import { SessionRunner } from "./session/runner/index.js"
|
||||
import { SessionStore } from "./session/store.js"
|
||||
import { decodeMessageRow } from "./session/history.js"
|
||||
import { SessionExecution } from "./session/execution.js"
|
||||
import {
|
||||
AttachmentError,
|
||||
@@ -93,6 +94,7 @@ type CompactInput = Parameters<Session.Handle["compact"]>[0] & { sessionID: Sess
|
||||
type ForkInput = {
|
||||
sessionID: SessionSchema.ID
|
||||
boundary: SessionSchema.ForkRequestBoundary
|
||||
filter?: (messages: readonly SessionMessage.Info[]) => readonly SessionMessage.Info[]
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -293,7 +295,7 @@ const layer = Layer.effect(
|
||||
fork: Effect.fn("Session.fork")(function* (input) {
|
||||
const parent = yield* result.get(input.sessionID)
|
||||
const boundary = yield* db
|
||||
.select({ id: SessionMessageTable.id })
|
||||
.select({ id: SessionMessageTable.id, seq: SessionMessageTable.seq })
|
||||
.from(SessionMessageTable)
|
||||
.where(
|
||||
and(
|
||||
@@ -311,6 +313,31 @@ const layer = Layer.effect(
|
||||
messageID: input.boundary.messageID,
|
||||
})
|
||||
if (!boundary) return yield* new ForkEmptyError({ sessionID: input.sessionID })
|
||||
const messages = input.filter
|
||||
? input.filter(
|
||||
yield* db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, parent.id),
|
||||
input.boundary.type === "before"
|
||||
? lt(SessionMessageTable.seq, boundary.seq)
|
||||
: lte(SessionMessageTable.seq, boundary.seq),
|
||||
sql`${SessionMessageTable.type} != 'assistant' or json_extract(${SessionMessageTable.data}, '$.time.completed') is not null`,
|
||||
sql`${SessionMessageTable.type} != 'shell' or json_extract(${SessionMessageTable.data}, '$.status') != 'running'`,
|
||||
sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') != 'running'`,
|
||||
),
|
||||
)
|
||||
.orderBy(asc(SessionMessageTable.seq))
|
||||
.all()
|
||||
.pipe(
|
||||
Effect.orDie,
|
||||
Effect.flatMap((rows) => Effect.forEach(rows, decodeMessageRow)),
|
||||
Effect.orDie,
|
||||
),
|
||||
)
|
||||
: undefined
|
||||
const sessionID = SessionSchema.ID.create()
|
||||
const inherited = yield* db
|
||||
.transaction(() =>
|
||||
@@ -327,6 +354,7 @@ const layer = Layer.effect(
|
||||
sessionID,
|
||||
parentID: parent.id,
|
||||
boundary: { ...input.boundary, messageID: boundary.id },
|
||||
messages: messages === undefined ? undefined : Schema.encodeSync(Schema.Array(SessionMessage.Info))(messages),
|
||||
...inherited,
|
||||
})
|
||||
return yield* result.get(sessionID).pipe(Effect.orDie)
|
||||
|
||||
@@ -179,6 +179,32 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
||||
if (event.data.instructionEntries)
|
||||
yield* InstructionEntry.initialize(db, event.data.sessionID, event.data.instructionEntries, event.created)
|
||||
|
||||
if (event.data.messages !== undefined) {
|
||||
if (event.data.messages.length > 0) {
|
||||
yield* db
|
||||
.insert(SessionMessageTable)
|
||||
.values(
|
||||
event.data.messages.map((message, index) => {
|
||||
const { id: _, type, ...data } = message
|
||||
return {
|
||||
id: SessionMessage.ID.make(`${SessionMessage.ID.fromEvent(event.id)}_${index + 1}`),
|
||||
session_id: event.data.sessionID,
|
||||
type,
|
||||
seq: index + 1,
|
||||
time_created: data.time.created,
|
||||
data,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* Bus.reserveSequence(db, event.data.sessionID, event.data.messages.length)
|
||||
}
|
||||
if (event.data.instructions)
|
||||
yield* InstructionState.initialize(db, event.data.sessionID, event.durable.seq, event.data.instructions)
|
||||
return
|
||||
}
|
||||
|
||||
let cursor = -1
|
||||
while (copiedSeq !== undefined) {
|
||||
const rows = yield* db
|
||||
|
||||
@@ -162,6 +162,7 @@ export function host(overrides: Overrides = {}): Plugin.Context {
|
||||
session: {
|
||||
hook: overrides.session?.hook ?? (() => Effect.die("unused session.hook")),
|
||||
create: overrides.session?.create ?? (() => Effect.die("unused session.create")),
|
||||
fork: overrides.session?.fork ?? (() => Effect.die("unused session.fork")),
|
||||
get: overrides.session?.get ?? (() => Effect.die("unused session.get")),
|
||||
switchAgent: overrides.session?.switchAgent ?? (() => Effect.die("unused session.switchAgent")),
|
||||
switchModel: overrides.session?.switchModel ?? (() => Effect.die("unused session.switchModel")),
|
||||
|
||||
@@ -40,6 +40,9 @@ import { offlineModels } from "./fixture/models"
|
||||
import { promptLocationNode } from "./fixture/prompt-location"
|
||||
import { globalProjectNode } from "./lib/project"
|
||||
import { tmpdirScoped } from "./fixture/tmpdir"
|
||||
import { Plugin } from "@opencode/plugin"
|
||||
import { PluginPromise } from "@opencode/core/plugin/promise"
|
||||
import { host } from "./plugin/host"
|
||||
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(
|
||||
@@ -561,6 +564,98 @@ describe("Session.create", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("freezes filtered fork history and replays it without invoking the callback", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
const bus = yield* Bus.Service
|
||||
const { db } = yield* Database.Service
|
||||
const parent = yield* session.create({ location })
|
||||
yield* session.prompt({ sessionID: parent.id, text: "First", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
yield* session.synthetic({ sessionID: parent.id, text: "Original note", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
const last = yield* session.prompt({ sessionID: parent.id, text: "Excluded by boundary", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
const calls: number[] = []
|
||||
const forked = yield* session.fork({
|
||||
sessionID: parent.id,
|
||||
boundary: { type: "before", messageID: last.id },
|
||||
filter: (messages) => {
|
||||
calls.push(messages.length)
|
||||
return messages.flatMap((message) =>
|
||||
message.type === "synthetic" ? [{ ...message, text: "Filtered note" }] : [],
|
||||
)
|
||||
},
|
||||
})
|
||||
const original = yield* session.context(forked.id)
|
||||
expect(calls).toEqual([2])
|
||||
expect(original).toMatchObject([{ type: "synthetic", text: "Filtered note" }])
|
||||
const event = Array.from(yield* Stream.runCollect(logEvents(session, forked.id)))[0]
|
||||
if (event.type !== "session.forked") return yield* Effect.die(new Error("Fork event not found"))
|
||||
expect(typeof event.data.messages?.[0].time.created).toBe("number")
|
||||
expect((yield* session.context(parent.id))[1]).toMatchObject({ text: "Original note" })
|
||||
const recorded = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, forked.id)).get()
|
||||
if (!recorded) return yield* Effect.die(new Error("Fork event not found"))
|
||||
yield* bus.remove(forked.id)
|
||||
yield* db.delete(SessionTable).where(eq(SessionTable.id, forked.id)).run()
|
||||
yield* bus.replay({
|
||||
id: recorded.id,
|
||||
created: recorded.created,
|
||||
aggregateID: recorded.aggregate_id,
|
||||
seq: recorded.seq,
|
||||
type: recorded.type,
|
||||
data: recorded.data,
|
||||
})
|
||||
expect(yield* session.context(forked.id)).toEqual(original)
|
||||
expect(calls).toEqual([2])
|
||||
yield* session.prompt({ sessionID: forked.id, text: "Continue", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, forked.id, "steer")
|
||||
expect(yield* session.context(forked.id)).toMatchObject([
|
||||
{ type: "synthetic", text: "Filtered note" },
|
||||
{ type: "user", text: "Continue" },
|
||||
])
|
||||
const empty = yield* session.fork({
|
||||
sessionID: parent.id,
|
||||
boundary: { type: "through" },
|
||||
filter: () => [],
|
||||
})
|
||||
expect(yield* session.context(empty.id)).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("exposes filtered forks to Promise plugins with decoded callback messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
const bus = yield* Bus.Service
|
||||
const { db } = yield* Database.Service
|
||||
const parent = yield* session.create({ location })
|
||||
yield* session.prompt({ sessionID: parent.id, text: "Keep", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
yield* session.synthetic({ sessionID: parent.id, text: "Drop", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
const plugin = PluginPromise.fromPromise(
|
||||
Plugin.define({
|
||||
id: "filtered-fork",
|
||||
async setup(ctx) {
|
||||
const fork = await ctx.session.fork({
|
||||
sessionID: parent.id,
|
||||
boundary: { type: "through" },
|
||||
filter: (messages) => {
|
||||
expect(DateTime.isDateTime(messages[0].time.created)).toBe(true)
|
||||
return messages.filter((message) => message.type === "user")
|
||||
},
|
||||
})
|
||||
expect(typeof fork.time.created).toBe("number")
|
||||
expect(await ctx.session.context({ sessionID: fork.id })).toMatchObject([{ type: "user", text: "Keep" }])
|
||||
},
|
||||
}),
|
||||
)
|
||||
yield* plugin.effect(
|
||||
host({ session: { fork: session.fork, context: (input) => session.context(input.sessionID) } }),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps a fork untitled when its parent is untitled", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
|
||||
@@ -98,5 +98,11 @@ export type SessionDomain = Pick<
|
||||
| "wait"
|
||||
| "context"
|
||||
> & {
|
||||
readonly fork: (
|
||||
input: Parameters<SessionApi<unknown>["fork"]>[0] & {
|
||||
/** Select or transform settled history in chronological order. Runs once before the fork is recorded. */
|
||||
readonly filter?: (messages: readonly SessionMessage.Info[]) => readonly SessionMessage.Info[]
|
||||
},
|
||||
) => ReturnType<SessionApi<unknown>["fork"]>
|
||||
readonly hook: ModelHooks<SessionHooks>
|
||||
}
|
||||
|
||||
@@ -566,6 +566,12 @@ export function fromPromise(plugin: Plugin) {
|
||||
host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))), options),
|
||||
),
|
||||
create: adaptApiMethod(SessionEndpoints["session.create"], host.session.create),
|
||||
fork: (input) =>
|
||||
adaptApiMethod<PromiseContext["session"]["fork"]>(
|
||||
SessionEndpoints["session.fork"],
|
||||
(request: Parameters<typeof host.session.fork>[0]) =>
|
||||
host.session.fork({ ...request, filter: input.filter }),
|
||||
)(input),
|
||||
get: adaptApiMethod(SessionEndpoints["session.get"], host.session.get),
|
||||
switchAgent: adaptApiMethod(SessionEndpoints["session.switchAgent"], host.session.switchAgent),
|
||||
switchModel: adaptApiMethod(SessionEndpoints["session.switchModel"], host.session.switchModel),
|
||||
|
||||
@@ -98,5 +98,11 @@ export type SessionDomain = Pick<
|
||||
| "wait"
|
||||
| "context"
|
||||
> & {
|
||||
readonly fork: (
|
||||
input: Parameters<SessionApi["fork"]>[0] & {
|
||||
/** Select or transform settled history in chronological order. Uses decoded Session messages, like session hooks. */
|
||||
readonly filter?: (messages: readonly SessionMessage.Info[]) => readonly SessionMessage.Info[]
|
||||
},
|
||||
) => ReturnType<SessionApi["fork"]>
|
||||
readonly hook: ModelHooks<SessionHooks>
|
||||
}
|
||||
|
||||
@@ -1621,14 +1621,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
"$ref": "#/components/schemas/SessionNotFoundErrorEncoded"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,8 @@ export const Forked = Event.durable({
|
||||
...Base,
|
||||
parentID: SessionID,
|
||||
boundary: SessionFork.Boundary,
|
||||
/** Frozen plugin-selected history. Omitted for ordinary boundary-based forks. */
|
||||
messages: Schema.Array(SessionMessage.InfoEncoded).pipe(optional),
|
||||
instructions: Instruction.Values.pipe(optional),
|
||||
instructionEntries: InstructionEntry.Snapshot.pipe(optional),
|
||||
},
|
||||
|
||||
@@ -296,3 +296,6 @@ export type Info =
|
||||
| Assistant
|
||||
| Compaction
|
||||
export type Type = Info["type"]
|
||||
|
||||
export const InfoEncoded = Schema.toEncoded(Info).annotate({ identifier: "Session.Message.Info.Encoded" })
|
||||
export type InfoEncoded = typeof InfoEncoded.Type
|
||||
|
||||
@@ -1621,14 +1621,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
"$ref": "#/components/schemas/SessionNotFoundErrorEncoded"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1621,14 +1621,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
"$ref": "#/components/schemas/SessionNotFoundErrorEncoded"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user