Compare commits
3 commits
dev
...
nxl/sessio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7faad0f8f | ||
|
|
fd14c00481 | ||
|
|
522322980e |
5 changed files with 66 additions and 10 deletions
|
|
@ -4,6 +4,8 @@ import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||||
import { Effect, Layer, Context, Schema } from "effect"
|
import { Effect, Layer, Context, Schema } from "effect"
|
||||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
|
import { NotFoundError } from "@/storage/storage"
|
||||||
|
import { Session } from "./session"
|
||||||
|
|
||||||
export const Info = Schema.Union([
|
export const Info = Schema.Union([
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
|
|
@ -37,6 +39,7 @@ export const Event = {
|
||||||
schema: {
|
schema: {
|
||||||
sessionID: SessionID,
|
sessionID: SessionID,
|
||||||
status: Info,
|
status: Info,
|
||||||
|
parentID: Schema.optional(SessionID),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
// deprecated
|
// deprecated
|
||||||
|
|
@ -60,6 +63,7 @@ export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2Bridge.Service
|
const events = yield* EventV2Bridge.Service
|
||||||
|
const sessions = yield* Session.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make(
|
const state = yield* InstanceState.make(
|
||||||
Effect.fn("SessionStatus.state")(() => Effect.succeed(new Map<SessionID, Info>())),
|
Effect.fn("SessionStatus.state")(() => Effect.succeed(new Map<SessionID, Info>())),
|
||||||
|
|
@ -76,7 +80,12 @@ export const layer = Layer.effect(
|
||||||
|
|
||||||
const set = Effect.fn("SessionStatus.set")(function* (sessionID: SessionID, status: Info) {
|
const set = Effect.fn("SessionStatus.set")(function* (sessionID: SessionID, status: Info) {
|
||||||
const data = yield* InstanceState.get(state)
|
const data = yield* InstanceState.get(state)
|
||||||
yield* events.publish(Event.Status, { sessionID, status })
|
const session = yield* sessions.get(sessionID).pipe(Effect.catchIf(NotFoundError.isInstance, () => Effect.void))
|
||||||
|
yield* events.publish(Event.Status, {
|
||||||
|
sessionID,
|
||||||
|
status,
|
||||||
|
...(session?.parentID ? { parentID: session.parentID } : {}),
|
||||||
|
})
|
||||||
if (status.type === "idle") {
|
if (status.type === "idle") {
|
||||||
yield* events.publish(Event.Idle, { sessionID })
|
yield* events.publish(Event.Idle, { sessionID })
|
||||||
data.delete(sessionID)
|
data.delete(sessionID)
|
||||||
|
|
@ -89,6 +98,6 @@ export const layer = Layer.effect(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(Session.defaultLayer), Layer.provide(EventV2Bridge.defaultLayer))
|
||||||
|
|
||||||
export * as SessionStatus from "./status"
|
export * as SessionStatus from "./status"
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,7 @@ function withCompaction(options?: CompactionProcessOptions) {
|
||||||
|
|
||||||
function compactionProcessLayer(options?: CompactionProcessOptions) {
|
function compactionProcessLayer(options?: CompactionProcessOptions) {
|
||||||
const events = EventV2Bridge.defaultLayer
|
const events = EventV2Bridge.defaultLayer
|
||||||
const status = SessionStatus.layer.pipe(Layer.provide(events))
|
const status = SessionStatus.layer.pipe(Layer.provide(SessionNs.defaultLayer), Layer.provide(events))
|
||||||
const processor = options?.llm
|
const processor = options?.llm
|
||||||
? SessionProcessorModule.SessionProcessor.layer.pipe(
|
? SessionProcessorModule.SessionProcessor.layer.pipe(
|
||||||
Layer.provide(summary),
|
Layer.provide(summary),
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,10 @@ const assistant = Effect.fn("TestSession.assistant")(function* (
|
||||||
return msg
|
return msg
|
||||||
})
|
})
|
||||||
|
|
||||||
const status = SessionStatus.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer))
|
const status = SessionStatus.layer.pipe(
|
||||||
|
Layer.provide(Session.defaultLayer),
|
||||||
|
Layer.provideMerge(EventV2Bridge.defaultLayer),
|
||||||
|
)
|
||||||
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
|
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
|
||||||
const deps = Layer.mergeAll(
|
const deps = Layer.mergeAll(
|
||||||
Session.defaultLayer,
|
Session.defaultLayer,
|
||||||
|
|
@ -588,15 +591,18 @@ it.live("session.processor effect tests publish retry status updates", () =>
|
||||||
yield* llm.error(503, { error: "boom" })
|
yield* llm.error(503, { error: "boom" })
|
||||||
yield* llm.text("")
|
yield* llm.text("")
|
||||||
|
|
||||||
const chat = yield* session.create({})
|
const root = yield* session.create({})
|
||||||
|
const chat = yield* session.create({ parentID: root.id })
|
||||||
const parent = yield* user(chat.id, "retry")
|
const parent = yield* user(chat.id, "retry")
|
||||||
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
|
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
|
||||||
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
|
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
|
||||||
const states: number[] = []
|
const states: Array<{ attempt: number; parentID?: SessionID }> = []
|
||||||
const off = yield* events.listen((evt) => {
|
const off = yield* events.listen((evt) => {
|
||||||
if (evt.type !== SessionStatus.Event.Status.type) return Effect.void
|
if (evt.type !== SessionStatus.Event.Status.type) return Effect.void
|
||||||
const data = evt.data as typeof SessionStatus.Event.Status.data.Type
|
const data = evt.data as typeof SessionStatus.Event.Status.data.Type
|
||||||
if (data.sessionID === chat.id && data.status.type === "retry") states.push(data.status.attempt)
|
if (data.sessionID === chat.id && data.status.type === "retry") {
|
||||||
|
states.push({ attempt: data.status.attempt, parentID: data.parentID })
|
||||||
|
}
|
||||||
return Effect.void
|
return Effect.void
|
||||||
})
|
})
|
||||||
const handle = yield* processors.create({
|
const handle = yield* processors.create({
|
||||||
|
|
@ -626,7 +632,7 @@ it.live("session.processor effect tests publish retry status updates", () =>
|
||||||
|
|
||||||
expect(value).toBe("continue")
|
expect(value).toBe("continue")
|
||||||
expect(yield* llm.calls).toBe(2)
|
expect(yield* llm.calls).toBe(2)
|
||||||
expect(states).toStrictEqual([1])
|
expect(states).toStrictEqual([{ attempt: 1, parentID: root.id }])
|
||||||
}),
|
}),
|
||||||
{ config: (url) => providerCfg(url) },
|
{ config: (url) => providerCfg(url) },
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,10 @@ const lsp = Layer.succeed(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const status = SessionStatus.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer))
|
const status = SessionStatus.layer.pipe(
|
||||||
|
Layer.provide(Session.defaultLayer),
|
||||||
|
Layer.provideMerge(EventV2Bridge.defaultLayer),
|
||||||
|
)
|
||||||
const run = SessionRunState.layer.pipe(Layer.provide(status))
|
const run = SessionRunState.layer.pipe(Layer.provide(status))
|
||||||
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
|
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
|
||||||
|
|
||||||
|
|
@ -867,6 +870,41 @@ it.instance(
|
||||||
3_000,
|
3_000,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.instance(
|
||||||
|
"loop status events identify child sessions through cancellation",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { llm } = yield* useServerConfig(providerCfg)
|
||||||
|
const prompt = yield* SessionPrompt.Service
|
||||||
|
const sessions = yield* Session.Service
|
||||||
|
const events = yield* EventV2Bridge.Service
|
||||||
|
const statuses: Array<typeof SessionStatus.Event.Status.data.Type> = []
|
||||||
|
|
||||||
|
yield* llm.hang
|
||||||
|
|
||||||
|
const parent = yield* sessions.create({})
|
||||||
|
const child = yield* sessions.create({ parentID: parent.id })
|
||||||
|
yield* user(child.id, "hi")
|
||||||
|
|
||||||
|
const off = yield* events.listen((event) => {
|
||||||
|
if (event.type !== SessionStatus.Event.Status.type) return Effect.void
|
||||||
|
const data = event.data as typeof SessionStatus.Event.Status.data.Type
|
||||||
|
if (data.sessionID === child.id) statuses.push(data)
|
||||||
|
return Effect.void
|
||||||
|
})
|
||||||
|
yield* Effect.addFinalizer(() => off)
|
||||||
|
|
||||||
|
const fiber = yield* prompt.loop({ sessionID: child.id }).pipe(Effect.forkChild)
|
||||||
|
yield* llm.wait(1)
|
||||||
|
yield* prompt.cancel(child.id)
|
||||||
|
yield* Fiber.await(fiber)
|
||||||
|
|
||||||
|
expect(statuses.some((event) => event.status.type === "idle")).toBe(true)
|
||||||
|
expect(statuses.every((event) => event.parentID === parent.id)).toBe(true)
|
||||||
|
}),
|
||||||
|
3_000,
|
||||||
|
)
|
||||||
|
|
||||||
// Cancel semantics
|
// Cancel semantics
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,10 @@ const lsp = Layer.succeed(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const status = SessionStatus.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer))
|
const status = SessionStatus.layer.pipe(
|
||||||
|
Layer.provide(Session.defaultLayer),
|
||||||
|
Layer.provideMerge(EventV2Bridge.defaultLayer),
|
||||||
|
)
|
||||||
const run = SessionRunState.layer.pipe(Layer.provide(status))
|
const run = SessionRunState.layer.pipe(Layer.provide(status))
|
||||||
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
|
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue