diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 2b092fc8fe..4705519daf 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -102,6 +102,8 @@ export namespace SessionPrompt {
const instruction = yield* Instruction.Service
const state = yield* SessionRunState.Service
const revert = yield* SessionRevert.Service
+ const sys = yield* SystemPrompt.Service
+ const llm = yield* LLM.Service
const run = {
promise: (effect: Effect.Effect) =>
@@ -180,21 +182,24 @@ export namespace SessionPrompt {
const msgs = onlySubtasks
? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }]
: yield* MessageV2.toModelMessagesEffect(context, mdl)
- const text = yield* Effect.promise(async (signal) => {
- const result = await LLM.stream({
+ const text = yield* llm
+ .stream({
agent: ag,
user: firstInfo,
system: [],
small: true,
tools: {},
model: mdl,
- abort: signal,
sessionID: input.session.id,
retries: 2,
messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...msgs],
})
- return result.text
- })
+ .pipe(
+ Stream.filter((e): e is Extract => e.type === "text-delta"),
+ Stream.map((e) => e.text),
+ Stream.mkString,
+ Effect.orDie,
+ )
const cleaned = text
.replace(/[\s\S]*?<\/think>\s*/g, "")
.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 })
const [skills, env, instructions, modelMsgs] = yield* Effect.all([
- Effect.promise(() => SystemPrompt.skills(agent)),
- Effect.promise(() => SystemPrompt.environment(model)),
+ sys.skills(agent),
+ Effect.sync(() => sys.environment(model)),
instruction.system().pipe(Effect.orDie),
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(Session.defaultLayer),
Layer.provide(SessionRevert.defaultLayer),
- Layer.provide(Agent.defaultLayer),
- Layer.provide(Bus.layer),
- Layer.provide(CrossSpawnSpawner.defaultLayer),
+ Layer.provide(
+ Layer.mergeAll(Agent.defaultLayer, SystemPrompt.defaultLayer, LLM.defaultLayer, Bus.layer, CrossSpawnSpawner.defaultLayer),
+ ),
),
)
const { runPromise } = makeRuntime(Service, defaultLayer)
diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts
index 09788f3cdb..2a001ba9b1 100644
--- a/packages/opencode/src/session/system.ts
+++ b/packages/opencode/src/session/system.ts
@@ -1,4 +1,4 @@
-import { Ripgrep } from "../file/ripgrep"
+import { Context, Effect, Layer } from "effect"
import { Instance } from "../project/instance"
@@ -33,44 +33,52 @@ export namespace SystemPrompt {
return [PROMPT_DEFAULT]
}
- export async function environment(model: Provider.Model) {
- const project = Instance.project
- return [
- [
- `You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
- `Here is some useful information about the environment you are running in:`,
- ``,
- ` Working directory: ${Instance.directory}`,
- ` Workspace root folder: ${Instance.worktree}`,
- ` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
- ` Platform: ${process.platform}`,
- ` Today's date: ${new Date().toDateString()}`,
- ``,
- ``,
- ` ${
- project.vcs === "git" && false
- ? await Ripgrep.tree({
- cwd: Instance.directory,
- limit: 50,
- })
- : ""
- }`,
- ``,
- ].join("\n"),
- ]
+ export interface Interface {
+ readonly environment: (model: Provider.Model) => string[]
+ readonly skills: (agent: Agent.Info) => Effect.Effect
}
- export async function skills(agent: Agent.Info) {
- if (Permission.disabled(["skill"], agent.permission).has("skill")) return
+ export class Service extends Context.Service()("@opencode/SystemPrompt") {}
- const list = await Skill.available(agent)
+ export const layer = Layer.effect(
+ Service,
+ Effect.gen(function* () {
+ const skill = yield* Skill.Service
- return [
- "Skills provide specialized instructions and workflows for specific tasks.",
- "Use the skill tool to load a skill when a task matches its description.",
- // the agents seem to ingest the information about skills a bit better if we present a more verbose
- // version of them here and a less verbose version in tool description, rather than vice versa.
- Skill.fmt(list, { verbose: true }),
- ].join("\n")
- }
+ return Service.of({
+ environment(model) {
+ const project = Instance.project
+ return [
+ [
+ `You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
+ `Here is some useful information about the environment you are running in:`,
+ ``,
+ ` Working directory: ${Instance.directory}`,
+ ` Workspace root folder: ${Instance.worktree}`,
+ ` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
+ ` Platform: ${process.platform}`,
+ ` Today's date: ${new Date().toDateString()}`,
+ ``,
+ ].join("\n"),
+ ]
+ },
+
+ skills: Effect.fn("SystemPrompt.skills")(function* (agent: Agent.Info) {
+ if (Permission.disabled(["skill"], agent.permission).has("skill")) return
+
+ const list = yield* skill.available(agent)
+
+ return [
+ "Skills provide specialized instructions and workflows for specific tasks.",
+ "Use the skill tool to load a skill when a task matches its description.",
+ // the agents seem to ingest the information about skills a bit better if we present a more verbose
+ // version of them here and a less verbose version in tool description, rather than vice versa.
+ Skill.fmt(list, { verbose: true }),
+ ].join("\n")
+ }),
+ })
+ }),
+ )
+
+ export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer))
}
diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts
index ba33cb086e..911c9f3443 100644
--- a/packages/opencode/test/session/prompt-effect.test.ts
+++ b/packages/opencode/test/session/prompt-effect.test.ts
@@ -31,6 +31,7 @@ import { SessionRunState } from "../../src/session/run-state"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { SessionStatus } from "../../src/session/status"
import { Skill } from "../../src/skill"
+import { SystemPrompt } from "../../src/session/system"
import { Shell } from "../../src/shell/shell"
import { Snapshot } from "../../src/snapshot"
import { ToolRegistry } from "../../src/tool/registry"
@@ -193,6 +194,7 @@ function makeHttp() {
Layer.provideMerge(registry),
Layer.provideMerge(trunc),
Layer.provide(Instruction.defaultLayer),
+ Layer.provide(SystemPrompt.defaultLayer),
Layer.provideMerge(deps),
),
)
diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts
index 1c242128e3..391d9d488c 100644
--- a/packages/opencode/test/session/snapshot-tool-race.test.ts
+++ b/packages/opencode/test/session/snapshot-tool-race.test.ts
@@ -41,6 +41,7 @@ import { Plugin } from "../../src/plugin"
import { Provider as ProviderSvc } from "../../src/provider/provider"
import { Question } from "../../src/question"
import { Skill } from "../../src/skill"
+import { SystemPrompt } from "../../src/session/system"
import { Todo } from "../../src/session/todo"
import { SessionCompaction } from "../../src/session/compaction"
import { Instruction } from "../../src/session/instruction"
@@ -157,6 +158,7 @@ function makeHttp() {
Layer.provideMerge(registry),
Layer.provideMerge(trunc),
Layer.provide(Instruction.defaultLayer),
+ Layer.provide(SystemPrompt.defaultLayer),
Layer.provideMerge(deps),
),
)
diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts
index 47f5f6fc25..6f1047a97d 100644
--- a/packages/opencode/test/session/system.test.ts
+++ b/packages/opencode/test/session/system.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test"
import path from "path"
+import { Effect } from "effect"
import { Agent } from "../../src/agent/agent"
import { Instance } from "../../src/project/instance"
import { SystemPrompt } from "../../src/session/system"
@@ -38,8 +39,13 @@ description: ${description}
directory: tmp.path,
fn: async () => {
const build = await Agent.get("build")
- const first = await SystemPrompt.skills(build!)
- const second = await SystemPrompt.skills(build!)
+ const runSkills = Effect.gen(function* () {
+ 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)