refactor(session): make SystemPrompt a proper Effect Service (#21992)
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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,44 +33,52 @@ export namespace SystemPrompt {
|
|||||||
return [PROMPT_DEFAULT]
|
return [PROMPT_DEFAULT]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function environment(model: Provider.Model) {
|
export interface Interface {
|
||||||
const project = Instance.project
|
readonly environment: (model: Provider.Model) => string[]
|
||||||
return [
|
readonly skills: (agent: Agent.Info) => Effect.Effect<string | undefined>
|
||||||
[
|
|
||||||
`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 async function skills(agent: Agent.Info) {
|
export class Service extends Context.Service<Service, Interface>()("@opencode/SystemPrompt") {}
|
||||||
if (Permission.disabled(["skill"], agent.permission).has("skill")) return
|
|
||||||
|
|
||||||
const list = await Skill.available(agent)
|
export const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const skill = yield* Skill.Service
|
||||||
|
|
||||||
return [
|
return Service.of({
|
||||||
"Skills provide specialized instructions and workflows for specific tasks.",
|
environment(model) {
|
||||||
"Use the skill tool to load a skill when a task matches its description.",
|
const project = Instance.project
|
||||||
// the agents seem to ingest the information about skills a bit better if we present a more verbose
|
return [
|
||||||
// version of them here and a less verbose version in tool description, rather than vice versa.
|
[
|
||||||
Skill.fmt(list, { verbose: true }),
|
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
|
||||||
].join("\n")
|
`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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user