feat: update session notices and skill reloads
This commit is contained in:
parent
98c09884bf
commit
fb884bb91e
29 changed files with 304 additions and 75 deletions
|
|
@ -2,7 +2,7 @@ import { $ } from "bun"
|
|||
import { describe, expect } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { ConfigProvider, Deferred, Duration, Effect, Fiber, Layer, Option, Stream } from "effect"
|
||||
import { Deferred, Duration, Effect, Fiber, Layer, Option, Stream } from "effect"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
|
|
@ -27,13 +27,6 @@ const configLayer = Layer.succeed(
|
|||
}),
|
||||
)
|
||||
|
||||
const flagsLayer = ConfigProvider.layer(
|
||||
ConfigProvider.fromUnknown({
|
||||
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false",
|
||||
}),
|
||||
)
|
||||
|
||||
function provide(directory: string, vcs?: Location.Interface["vcs"]) {
|
||||
const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
|
|
@ -44,7 +37,6 @@ function provide(directory: string, vcs?: Location.Interface["vcs"]) {
|
|||
Layer.provide(configLayer),
|
||||
Layer.provide(Git.defaultLayer),
|
||||
Layer.provide(locationLayer),
|
||||
Layer.provide(flagsLayer),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Database } from "@opencode-ai/core/database/database"
|
|||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Git } from "@opencode-ai/core/git"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Job } from "@opencode-ai/core/job"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { ProjectDirectories } from "@opencode-ai/core/project/directories"
|
||||
|
|
@ -29,6 +30,7 @@ const project = Project.layer.pipe(
|
|||
Layer.provide(ProjectDirectories.defaultLayer),
|
||||
)
|
||||
const sessions = SessionV2.layer.pipe(
|
||||
Layer.provide(Job.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Deferred, Effect, Fiber, Layer } from "effect"
|
|||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Job } from "@opencode-ai/core/job"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { PermissionTable } from "@opencode-ai/core/permission/sql"
|
||||
|
|
@ -24,6 +25,7 @@ const current = Layer.succeed(
|
|||
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
|
||||
)
|
||||
const sessions = SessionV2.layer.pipe(
|
||||
Layer.provide(Job.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { OpenAIChat } from "@opencode-ai/llm/protocols"
|
|||
import { Config } from "@opencode-ai/core/config"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Job } from "@opencode-ai/core/job"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||
import type { LocationServices } from "@opencode-ai/core/location-services"
|
||||
|
|
@ -60,6 +61,7 @@ const locations = Layer.effect(
|
|||
),
|
||||
)
|
||||
const sessions = SessionV2.layer.pipe(
|
||||
Layer.provide(Job.layer),
|
||||
Layer.provide(locations),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { asc, eq } from "drizzle-orm"
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||
import { Job } from "@opencode-ai/core/job"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
|
|
@ -36,6 +37,7 @@ const projects = Layer.succeed(
|
|||
}),
|
||||
)
|
||||
const sessions = SessionV2.layer.pipe(
|
||||
Layer.provide(Job.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
|
|
@ -182,11 +184,9 @@ describe("SessionV2.create", () => {
|
|||
expect((yield* session.context(parent.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"])
|
||||
expect((yield* session.context(forked.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"])
|
||||
expect((yield* session.context(forked.id)).at(-1)).toMatchObject({ text: "Child continues" })
|
||||
expect((yield* session.history({ sessionID: forked.id, limit: 10 })).events.map((event) => event.durable?.seq)).toEqual([
|
||||
0,
|
||||
4,
|
||||
5,
|
||||
])
|
||||
expect(
|
||||
(yield* session.history({ sessionID: forked.id, limit: 10 })).events.map((event) => event.durable?.seq),
|
||||
).toEqual([0, 4, 5])
|
||||
expect(yield* SessionInput.find(db, admitted.id)).toMatchObject({ sessionID: parent.id })
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect } from "bun:test"
|
|||
import { Effect, Layer, Schema } from "effect"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Job } from "@opencode-ai/core/job"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
|
|
@ -23,6 +24,7 @@ const projects = Layer.succeed(
|
|||
}),
|
||||
)
|
||||
const sessions = SessionV2.layer.pipe(
|
||||
Layer.provide(Job.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { eq } from "drizzle-orm"
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||
import { Job } from "@opencode-ai/core/job"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
|
@ -45,6 +46,7 @@ const execution = Layer.succeed(
|
|||
}),
|
||||
)
|
||||
const sessions = SessionV2.layer.pipe(
|
||||
Layer.provide(Job.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
|
|
@ -114,7 +116,11 @@ const eventCount = (type: string) =>
|
|||
|
||||
const encodeMessage = Schema.encodeSync(SessionMessage.Message)
|
||||
const assistantRow = (id: SessionMessage.ID, seq: number) => {
|
||||
const { id: _, type, ...data } = encodeMessage(
|
||||
const {
|
||||
id: _,
|
||||
type,
|
||||
...data
|
||||
} = encodeMessage(
|
||||
SessionMessage.Assistant.make({
|
||||
id,
|
||||
type: "assistant",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Auth, LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||
import { Job } from "@opencode-ai/core/job"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
|
|
@ -111,6 +112,7 @@ const execution = Layer.effect(
|
|||
}),
|
||||
).pipe(Layer.provide(runner))
|
||||
const sessions = SessionV2.layer.pipe(
|
||||
Layer.provide(Job.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Job } from "@opencode-ai/core/job"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
|
|
@ -266,6 +267,7 @@ const execution = Layer.effect(
|
|||
}),
|
||||
).pipe(Layer.provide(runner))
|
||||
const sessions = SessionV2.layer.pipe(
|
||||
Layer.provide(Job.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect } from "bun:test"
|
|||
import { Effect, Layer } from "effect"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Job } from "@opencode-ai/core/job"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
|
|
@ -21,6 +22,7 @@ const execution = Layer.mock(SessionExecution.Service, {
|
|||
awaitIdle: (sessionID) => Effect.sync(() => awaited.push(sessionID)),
|
||||
})
|
||||
const sessions = SessionV2.layer.pipe(
|
||||
Layer.provide(Job.layer),
|
||||
Layer.provide(locationServiceMapLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Deferred, Effect, Fiber, Layer, Stream } from "effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
|
||||
import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
|
|
@ -24,7 +26,7 @@ const discovery = Layer.succeed(
|
|||
}),
|
||||
)
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(LayerNode.group([SkillV2.node, AgentV2.node]), [
|
||||
AppNodeBuilder.build(LayerNode.group([SkillV2.node, AgentV2.node, EventV2.node]), [
|
||||
LayerNode.replace(SkillDiscovery.layer, discovery),
|
||||
]),
|
||||
)
|
||||
|
|
@ -40,6 +42,19 @@ description: ${description}
|
|||
)
|
||||
}
|
||||
|
||||
function waitForSkillUpdate() {
|
||||
return Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const deferred = yield* Deferred.make<void>()
|
||||
const fiber = yield* events.subscribe(SkillV2.Event.Updated).pipe(
|
||||
Stream.runForEach(() => Deferred.succeed(deferred, undefined).pipe(Effect.asVoid)),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
yield* Effect.yieldNow
|
||||
return { deferred, fiber }
|
||||
})
|
||||
}
|
||||
|
||||
describe("SkillV2", () => {
|
||||
it.live("registers sources and resolves later source precedence", () =>
|
||||
Effect.acquireRelease(
|
||||
|
|
@ -124,4 +139,41 @@ describe("SkillV2", () => {
|
|||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("invalidates cached skills and publishes updates for watcher changes", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.promise(async () => {
|
||||
await fs.mkdir(path.join(tmp.path, "deploy"), { recursive: true })
|
||||
await write(tmp.path, "deploy", "Initial deploy")
|
||||
})
|
||||
|
||||
const events = yield* EventV2.Service
|
||||
const skill = yield* SkillV2.Service
|
||||
yield* skill.transform((editor) => editor.source({ type: "directory", path: AbsolutePath.make(tmp.path) }))
|
||||
|
||||
expect((yield* skill.list()).find((item) => item.name === "deploy")?.description).toBe("Initial deploy")
|
||||
|
||||
const file = path.join(tmp.path, "deploy", "SKILL.md")
|
||||
yield* Effect.promise(() => write(tmp.path, "deploy", "Updated deploy"))
|
||||
expect((yield* skill.list()).find((item) => item.name === "deploy")?.description).toBe("Initial deploy")
|
||||
|
||||
yield* Effect.acquireUseRelease(
|
||||
waitForSkillUpdate(),
|
||||
({ deferred }) =>
|
||||
events
|
||||
.publish(FileSystemWatcher.Event.Updated, { file, event: "change" })
|
||||
.pipe(Effect.andThen(Deferred.await(deferred)), Effect.timeout("1 second")),
|
||||
({ fiber }) => Fiber.interrupt(fiber),
|
||||
)
|
||||
|
||||
expect((yield* skill.list()).find((item) => item.name === "deploy")?.description).toBe("Updated deploy")
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue