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)