Skip to content

Commit ccb0b32

Browse files
authored
refactor(session): make SystemPrompt a proper Effect Service (#21992)
1 parent 5ee7eda commit ccb0b32

5 files changed

Lines changed: 72 additions & 49 deletions

File tree

packages/opencode/src/session/prompt.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ export namespace SessionPrompt {
102102
const instruction = yield* Instruction.Service
103103
const state = yield* SessionRunState.Service
104104
const revert = yield* SessionRevert.Service
105+
const sys = yield* SystemPrompt.Service
106+
const llm = yield* LLM.Service
105107

106108
const run = {
107109
promise: <A, E>(effect: Effect.Effect<A, E>) =>
@@ -180,21 +182,24 @@ export namespace SessionPrompt {
180182
const msgs = onlySubtasks
181183
? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }]
182184
: yield* MessageV2.toModelMessagesEffect(context, mdl)
183-
const text = yield* Effect.promise(async (signal) => {
184-
const result = await LLM.stream({
185+
const text = yield* llm
186+
.stream({
185187
agent: ag,
186188
user: firstInfo,
187189
system: [],
188190
small: true,
189191
tools: {},
190192
model: mdl,
191-
abort: signal,
192193
sessionID: input.session.id,
193194
retries: 2,
194195
messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...msgs],
195196
})
196-
return result.text
197-
})
197+
.pipe(
198+
Stream.filter((e): e is Extract<LLM.Event, { type: "text-delta" }> => e.type === "text-delta"),
199+
Stream.map((e) => e.text),
200+
Stream.mkString,
201+
Effect.orDie,
202+
)
198203
const cleaned = text
199204
.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
200205
.split("\n")
@@ -1462,8 +1467,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
14621467
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
14631468

14641469
const [skills, env, instructions, modelMsgs] = yield* Effect.all([
1465-
Effect.promise(() => SystemPrompt.skills(agent)),
1466-
Effect.promise(() => SystemPrompt.environment(model)),
1470+
sys.skills(agent),
1471+
Effect.sync(() => sys.environment(model)),
14671472
instruction.system().pipe(Effect.orDie),
14681473
MessageV2.toModelMessagesEffect(msgs, model),
14691474
])
@@ -1687,9 +1692,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
16871692
Layer.provide(Plugin.defaultLayer),
16881693
Layer.provide(Session.defaultLayer),
16891694
Layer.provide(SessionRevert.defaultLayer),
1690-
Layer.provide(Agent.defaultLayer),
1691-
Layer.provide(Bus.layer),
1692-
Layer.provide(CrossSpawnSpawner.defaultLayer),
1695+
Layer.provide(
1696+
Layer.mergeAll(Agent.defaultLayer, SystemPrompt.defaultLayer, LLM.defaultLayer, Bus.layer, CrossSpawnSpawner.defaultLayer),
1697+
),
16931698
),
16941699
)
16951700
const { runPromise } = makeRuntime(Service, defaultLayer)
Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Ripgrep } from "../file/ripgrep"
1+
import { Context, Effect, Layer } from "effect"
22

33
import { Instance } from "../project/instance"
44

@@ -33,44 +33,52 @@ export namespace SystemPrompt {
3333
return [PROMPT_DEFAULT]
3434
}
3535

36-
export async function environment(model: Provider.Model) {
37-
const project = Instance.project
38-
return [
39-
[
40-
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
41-
`Here is some useful information about the environment you are running in:`,
42-
`<env>`,
43-
` Working directory: ${Instance.directory}`,
44-
` Workspace root folder: ${Instance.worktree}`,
45-
` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
46-
` Platform: ${process.platform}`,
47-
` Today's date: ${new Date().toDateString()}`,
48-
`</env>`,
49-
`<directories>`,
50-
` ${
51-
project.vcs === "git" && false
52-
? await Ripgrep.tree({
53-
cwd: Instance.directory,
54-
limit: 50,
55-
})
56-
: ""
57-
}`,
58-
`</directories>`,
59-
].join("\n"),
60-
]
36+
export interface Interface {
37+
readonly environment: (model: Provider.Model) => string[]
38+
readonly skills: (agent: Agent.Info) => Effect.Effect<string | undefined>
6139
}
6240

63-
export async function skills(agent: Agent.Info) {
64-
if (Permission.disabled(["skill"], agent.permission).has("skill")) return
41+
export class Service extends Context.Service<Service, Interface>()("@opencode/SystemPrompt") {}
6542

66-
const list = await Skill.available(agent)
43+
export const layer = Layer.effect(
44+
Service,
45+
Effect.gen(function* () {
46+
const skill = yield* Skill.Service
6747

68-
return [
69-
"Skills provide specialized instructions and workflows for specific tasks.",
70-
"Use the skill tool to load a skill when a task matches its description.",
71-
// the agents seem to ingest the information about skills a bit better if we present a more verbose
72-
// version of them here and a less verbose version in tool description, rather than vice versa.
73-
Skill.fmt(list, { verbose: true }),
74-
].join("\n")
75-
}
48+
return Service.of({
49+
environment(model) {
50+
const project = Instance.project
51+
return [
52+
[
53+
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
54+
`Here is some useful information about the environment you are running in:`,
55+
`<env>`,
56+
` Working directory: ${Instance.directory}`,
57+
` Workspace root folder: ${Instance.worktree}`,
58+
` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
59+
` Platform: ${process.platform}`,
60+
` Today's date: ${new Date().toDateString()}`,
61+
`</env>`,
62+
].join("\n"),
63+
]
64+
},
65+
66+
skills: Effect.fn("SystemPrompt.skills")(function* (agent: Agent.Info) {
67+
if (Permission.disabled(["skill"], agent.permission).has("skill")) return
68+
69+
const list = yield* skill.available(agent)
70+
71+
return [
72+
"Skills provide specialized instructions and workflows for specific tasks.",
73+
"Use the skill tool to load a skill when a task matches its description.",
74+
// the agents seem to ingest the information about skills a bit better if we present a more verbose
75+
// version of them here and a less verbose version in tool description, rather than vice versa.
76+
Skill.fmt(list, { verbose: true }),
77+
].join("\n")
78+
}),
79+
})
80+
}),
81+
)
82+
83+
export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer))
7684
}

packages/opencode/test/session/prompt-effect.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { SessionRunState } from "../../src/session/run-state"
3131
import { MessageID, PartID, SessionID } from "../../src/session/schema"
3232
import { SessionStatus } from "../../src/session/status"
3333
import { Skill } from "../../src/skill"
34+
import { SystemPrompt } from "../../src/session/system"
3435
import { Shell } from "../../src/shell/shell"
3536
import { Snapshot } from "../../src/snapshot"
3637
import { ToolRegistry } from "../../src/tool/registry"
@@ -193,6 +194,7 @@ function makeHttp() {
193194
Layer.provideMerge(registry),
194195
Layer.provideMerge(trunc),
195196
Layer.provide(Instruction.defaultLayer),
197+
Layer.provide(SystemPrompt.defaultLayer),
196198
Layer.provideMerge(deps),
197199
),
198200
)

packages/opencode/test/session/snapshot-tool-race.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { Plugin } from "../../src/plugin"
4141
import { Provider as ProviderSvc } from "../../src/provider/provider"
4242
import { Question } from "../../src/question"
4343
import { Skill } from "../../src/skill"
44+
import { SystemPrompt } from "../../src/session/system"
4445
import { Todo } from "../../src/session/todo"
4546
import { SessionCompaction } from "../../src/session/compaction"
4647
import { Instruction } from "../../src/session/instruction"
@@ -157,6 +158,7 @@ function makeHttp() {
157158
Layer.provideMerge(registry),
158159
Layer.provideMerge(trunc),
159160
Layer.provide(Instruction.defaultLayer),
161+
Layer.provide(SystemPrompt.defaultLayer),
160162
Layer.provideMerge(deps),
161163
),
162164
)

packages/opencode/test/session/system.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, test } from "bun:test"
22
import path from "path"
3+
import { Effect } from "effect"
34
import { Agent } from "../../src/agent/agent"
45
import { Instance } from "../../src/project/instance"
56
import { SystemPrompt } from "../../src/session/system"
@@ -38,8 +39,13 @@ description: ${description}
3839
directory: tmp.path,
3940
fn: async () => {
4041
const build = await Agent.get("build")
41-
const first = await SystemPrompt.skills(build!)
42-
const second = await SystemPrompt.skills(build!)
42+
const runSkills = Effect.gen(function* () {
43+
const svc = yield* SystemPrompt.Service
44+
return yield* svc.skills(build!)
45+
}).pipe(Effect.provide(SystemPrompt.defaultLayer))
46+
47+
const first = await Effect.runPromise(runSkills)
48+
const second = await Effect.runPromise(runSkills)
4349

4450
expect(first).toBe(second)
4551

0 commit comments

Comments
 (0)