Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: <A, E>(effect: Effect.Effect<A, E>) =>
Expand Down Expand Up @@ -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<LLM.Event, { type: "text-delta" }> => e.type === "text-delta"),
Stream.map((e) => e.text),
Stream.mkString,
Effect.orDie,
)
const cleaned = text
.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
.split("\n")
Expand Down Expand Up @@ -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),
])
Expand Down Expand Up @@ -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)
Expand Down
82 changes: 45 additions & 37 deletions packages/opencode/src/session/system.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Ripgrep } from "../file/ripgrep"
import { Context, Effect, Layer } from "effect"

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

Expand Down Expand Up @@ -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:`,
`<env>`,
` 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()}`,
`</env>`,
`<directories>`,
` ${
project.vcs === "git" && false
? await Ripgrep.tree({
cwd: Instance.directory,
limit: 50,
})
: ""
}`,
`</directories>`,
].join("\n"),
]
export interface Interface {
readonly environment: (model: Provider.Model) => string[]
readonly skills: (agent: Agent.Info) => Effect.Effect<string | undefined>
}

export async function skills(agent: Agent.Info) {
if (Permission.disabled(["skill"], agent.permission).has("skill")) return
export class Service extends Context.Service<Service, Interface>()("@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:`,
`<env>`,
` 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()}`,
`</env>`,
].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))
}
2 changes: 2 additions & 0 deletions packages/opencode/test/session/prompt-effect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -193,6 +194,7 @@ function makeHttp() {
Layer.provideMerge(registry),
Layer.provideMerge(trunc),
Layer.provide(Instruction.defaultLayer),
Layer.provide(SystemPrompt.defaultLayer),
Layer.provideMerge(deps),
),
)
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/test/session/snapshot-tool-race.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -157,6 +158,7 @@ function makeHttp() {
Layer.provideMerge(registry),
Layer.provideMerge(trunc),
Layer.provide(Instruction.defaultLayer),
Layer.provide(SystemPrompt.defaultLayer),
Layer.provideMerge(deps),
),
)
Expand Down
10 changes: 8 additions & 2 deletions packages/opencode/test/session/system.test.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)

Expand Down
Loading