refactor(session): make SystemPrompt a proper Effect Service (#21992)

This commit is contained in:
Kit Langton
2026-04-11 12:52:35 -04:00
committed by GitHub
parent 5ee7edaf9e
commit ccb0b320e1
5 changed files with 72 additions and 49 deletions
+15 -10
View File
@@ -102,6 +102,8 @@ export namespace SessionPrompt {
const instruction = yield* Instruction.Service const instruction = yield* Instruction.Service
const state = yield* SessionRunState.Service const state = yield* SessionRunState.Service
const revert = yield* SessionRevert.Service const revert = yield* SessionRevert.Service
const sys = yield* SystemPrompt.Service
const llm = yield* LLM.Service
const run = { const run = {
promise: <A, E>(effect: Effect.Effect<A, E>) => promise: <A, E>(effect: Effect.Effect<A, E>) =>
@@ -180,21 +182,24 @@ export namespace SessionPrompt {
const msgs = onlySubtasks const msgs = onlySubtasks
? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }] ? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }]
: yield* MessageV2.toModelMessagesEffect(context, mdl) : yield* MessageV2.toModelMessagesEffect(context, mdl)
const text = yield* Effect.promise(async (signal) => { const text = yield* llm
const result = await LLM.stream({ .stream({
agent: ag, agent: ag,
user: firstInfo, user: firstInfo,
system: [], system: [],
small: true, small: true,
tools: {}, tools: {},
model: mdl, model: mdl,
abort: signal,
sessionID: input.session.id, sessionID: input.session.id,
retries: 2, retries: 2,
messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...msgs], messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...msgs],
}) })
return result.text .pipe(
}) Stream.filter((e): e is Extract<LLM.Event, { type: "text-delta" }> => e.type === "text-delta"),
Stream.map((e) => e.text),
Stream.mkString,
Effect.orDie,
)
const cleaned = text const cleaned = text
.replace(/<think>[\s\S]*?<\/think>\s*/g, "") .replace(/<think>[\s\S]*?<\/think>\s*/g, "")
.split("\n") .split("\n")
@@ -1462,8 +1467,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
const [skills, env, instructions, modelMsgs] = yield* Effect.all([ const [skills, env, instructions, modelMsgs] = yield* Effect.all([
Effect.promise(() => SystemPrompt.skills(agent)), sys.skills(agent),
Effect.promise(() => SystemPrompt.environment(model)), Effect.sync(() => sys.environment(model)),
instruction.system().pipe(Effect.orDie), instruction.system().pipe(Effect.orDie),
MessageV2.toModelMessagesEffect(msgs, model), MessageV2.toModelMessagesEffect(msgs, model),
]) ])
@@ -1687,9 +1692,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
Layer.provide(Plugin.defaultLayer), Layer.provide(Plugin.defaultLayer),
Layer.provide(Session.defaultLayer), Layer.provide(Session.defaultLayer),
Layer.provide(SessionRevert.defaultLayer), Layer.provide(SessionRevert.defaultLayer),
Layer.provide(Agent.defaultLayer), Layer.provide(
Layer.provide(Bus.layer), Layer.mergeAll(Agent.defaultLayer, SystemPrompt.defaultLayer, LLM.defaultLayer, Bus.layer, CrossSpawnSpawner.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer), ),
), ),
) )
const { runPromise } = makeRuntime(Service, defaultLayer) const { runPromise } = makeRuntime(Service, defaultLayer)
+24 -16
View File
@@ -1,4 +1,4 @@
import { Ripgrep } from "../file/ripgrep" import { Context, Effect, Layer } from "effect"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
@@ -33,7 +33,20 @@ export namespace SystemPrompt {
return [PROMPT_DEFAULT] return [PROMPT_DEFAULT]
} }
export async function environment(model: Provider.Model) { export interface Interface {
readonly environment: (model: Provider.Model) => string[]
readonly skills: (agent: Agent.Info) => Effect.Effect<string | undefined>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/SystemPrompt") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const skill = yield* Skill.Service
return Service.of({
environment(model) {
const project = Instance.project const project = Instance.project
return [ return [
[ [
@@ -46,24 +59,14 @@ export namespace SystemPrompt {
` Platform: ${process.platform}`, ` Platform: ${process.platform}`,
` Today's date: ${new Date().toDateString()}`, ` Today's date: ${new Date().toDateString()}`,
`</env>`, `</env>`,
`<directories>`,
` ${
project.vcs === "git" && false
? await Ripgrep.tree({
cwd: Instance.directory,
limit: 50,
})
: ""
}`,
`</directories>`,
].join("\n"), ].join("\n"),
] ]
} },
export async function skills(agent: Agent.Info) { skills: Effect.fn("SystemPrompt.skills")(function* (agent: Agent.Info) {
if (Permission.disabled(["skill"], agent.permission).has("skill")) return if (Permission.disabled(["skill"], agent.permission).has("skill")) return
const list = await Skill.available(agent) const list = yield* skill.available(agent)
return [ return [
"Skills provide specialized instructions and workflows for specific tasks.", "Skills provide specialized instructions and workflows for specific tasks.",
@@ -72,5 +75,10 @@ export namespace SystemPrompt {
// version of them here and a less verbose version in tool description, rather than vice versa. // version of them here and a less verbose version in tool description, rather than vice versa.
Skill.fmt(list, { verbose: true }), Skill.fmt(list, { verbose: true }),
].join("\n") ].join("\n")
} }),
})
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer))
} }
@@ -31,6 +31,7 @@ import { SessionRunState } from "../../src/session/run-state"
import { MessageID, PartID, SessionID } from "../../src/session/schema" import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { SessionStatus } from "../../src/session/status" import { SessionStatus } from "../../src/session/status"
import { Skill } from "../../src/skill" import { Skill } from "../../src/skill"
import { SystemPrompt } from "../../src/session/system"
import { Shell } from "../../src/shell/shell" import { Shell } from "../../src/shell/shell"
import { Snapshot } from "../../src/snapshot" import { Snapshot } from "../../src/snapshot"
import { ToolRegistry } from "../../src/tool/registry" import { ToolRegistry } from "../../src/tool/registry"
@@ -193,6 +194,7 @@ function makeHttp() {
Layer.provideMerge(registry), Layer.provideMerge(registry),
Layer.provideMerge(trunc), Layer.provideMerge(trunc),
Layer.provide(Instruction.defaultLayer), Layer.provide(Instruction.defaultLayer),
Layer.provide(SystemPrompt.defaultLayer),
Layer.provideMerge(deps), Layer.provideMerge(deps),
), ),
) )
@@ -41,6 +41,7 @@ import { Plugin } from "../../src/plugin"
import { Provider as ProviderSvc } from "../../src/provider/provider" import { Provider as ProviderSvc } from "../../src/provider/provider"
import { Question } from "../../src/question" import { Question } from "../../src/question"
import { Skill } from "../../src/skill" import { Skill } from "../../src/skill"
import { SystemPrompt } from "../../src/session/system"
import { Todo } from "../../src/session/todo" import { Todo } from "../../src/session/todo"
import { SessionCompaction } from "../../src/session/compaction" import { SessionCompaction } from "../../src/session/compaction"
import { Instruction } from "../../src/session/instruction" import { Instruction } from "../../src/session/instruction"
@@ -157,6 +158,7 @@ function makeHttp() {
Layer.provideMerge(registry), Layer.provideMerge(registry),
Layer.provideMerge(trunc), Layer.provideMerge(trunc),
Layer.provide(Instruction.defaultLayer), Layer.provide(Instruction.defaultLayer),
Layer.provide(SystemPrompt.defaultLayer),
Layer.provideMerge(deps), Layer.provideMerge(deps),
), ),
) )
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import path from "path" import path from "path"
import { Effect } from "effect"
import { Agent } from "../../src/agent/agent" import { Agent } from "../../src/agent/agent"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { SystemPrompt } from "../../src/session/system" import { SystemPrompt } from "../../src/session/system"
@@ -38,8 +39,13 @@ description: ${description}
directory: tmp.path, directory: tmp.path,
fn: async () => { fn: async () => {
const build = await Agent.get("build") const build = await Agent.get("build")
const first = await SystemPrompt.skills(build!) const runSkills = Effect.gen(function* () {
const second = await SystemPrompt.skills(build!) const svc = yield* SystemPrompt.Service
return yield* svc.skills(build!)
}).pipe(Effect.provide(SystemPrompt.defaultLayer))
const first = await Effect.runPromise(runSkills)
const second = await Effect.runPromise(runSkills)
expect(first).toBe(second) expect(first).toBe(second)