diff --git a/packages/core/src/plugin/goal.ts b/packages/core/src/plugin/goal.ts deleted file mode 100644 index 55ca74338d1..00000000000 --- a/packages/core/src/plugin/goal.ts +++ /dev/null @@ -1,102 +0,0 @@ -export * as GoalPlugin from "./goal.js" - -import { define } from "@opencode/plugin/effect/plugin" -import type { Session } from "@opencode/schema/session" -import { Effect, Option, Schema, Stream } from "effect" - -const GoalState = Schema.Struct({ - goal: Schema.String, - active: Schema.Boolean, -}) - -const workers = new WeakSet() - -export const Plugin = define({ - id: "opencode.goal", - effect: Effect.fn(function* (ctx) { - const key = (sessionID: Session.ID) => `session/${sessionID}/goal` - const read = Effect.fn(function* (sessionID: Session.ID) { - return Option.getOrUndefined(Schema.decodeUnknownOption(GoalState)(yield* ctx.storage.get(key(sessionID)))) - }) - - const evaluate = Effect.fn(function* (sessionID: Session.ID) { - const state = yield* read(sessionID) - if (!state?.active) return - - const result = yield* ctx.session.generate({ - sessionID, - prompt: [ - "Evaluate progress toward the goal below using the current session context.", - "Reply with exactly COMPLETE if it is fully complete.", - "Otherwise reply with CONTINUE followed by one concise instruction for the next step.", - `Goal: ${state.goal}`, - ].join("\n\n"), - }) - const current = yield* read(sessionID) - if (!current?.active || current.goal !== state.goal) return - - const evaluation = result.text.trim() - if (/^COMPLETE\b/i.test(evaluation)) { - yield* ctx.session.synthetic({ - sessionID, - text: `Goal: ${state.goal}\n\nThe goal has been completed.`, - description: "Goal completed", - delivery: "steer", - resume: false, - }) - yield* ctx.storage.set(key(sessionID), { goal: state.goal, active: false }) - return - } - - yield* ctx.session.synthetic({ - sessionID, - text: [ - `Goal: ${state.goal}`, - `Next step: ${evaluation.replace(/^CONTINUE\s*/i, "")}`, - "Continue working autonomously until the goal is complete.", - ].join("\n\n"), - description: "Goal continuing", - delivery: "steer", - resume: true, - }) - }) - - if (!workers.has(ctx.app)) { - workers.add(ctx.app) - yield* ctx.event.subscribe().pipe( - Stream.mapEffect( - (event) => { - if (event.type !== "session.execution.succeeded") return Effect.void - return evaluate(event.data.sessionID).pipe( - Effect.catch((error) => - Effect.logError("goal evaluation failed", { sessionID: event.data.sessionID, error }), - ), - ) - }, - { concurrency: "unbounded", unordered: true }, - ), - Stream.runDrain, - Effect.forkDetach({ startImmediately: true }), - ) - } - - yield* ctx.command.transform((draft) => { - draft.add({ - name: "goal", - description: "Work autonomously toward a goal", - execute: Effect.fn(function* ({ sessionID, prompt, delivery }) { - const goal = prompt.text.trim() - if (!goal) return yield* Effect.fail(new Error("Usage: /goal ")) - yield* ctx.storage.set(key(sessionID), { goal, active: true }) - yield* ctx.session.synthetic({ - sessionID, - text: `Goal: ${goal}\n\nContinue until the goal is fully complete. Use tools and make changes as needed.`, - description: `Goal started: ${goal}`, - delivery, - resume: true, - }) - }), - }) - }) - }), -}) diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index d8e6e64f56a..d42a4d1d7e4 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -87,7 +87,6 @@ import { AgentPlugin } from "./agent.js" import BrowserPlugin from "@opencode/plugin-browser" import { CommandPlugin } from "./command.js" import { NativeCompactionPlugin } from "./compaction.js" -import { GoalPlugin } from "./goal.js" import { IdentityPlugin } from "./identity.js" import { PlanPlugin } from "./plan.js" import { ModelsDevPlugin } from "./models-dev.js" @@ -217,7 +216,6 @@ const pre = [ AgentPlugin.Plugin, PlanPlugin.Plugin, CommandPlugin.Plugin, - GoalPlugin.Plugin, SkillPlugin.Plugin, VcsHgPlugin.Plugin, ModelsDevPlugin, diff --git a/packages/core/src/session/history.ts b/packages/core/src/session/history.ts index ad8a7cf9896..aeb7cfc9f81 100644 --- a/packages/core/src/session/history.ts +++ b/packages/core/src/session/history.ts @@ -178,21 +178,4 @@ export const firstUserMessage = Effect.fn("SessionHistory.firstUserMessage")(fun return message?.type === "user" ? message : undefined }) -/** Returns the session's first synthetic message. */ -export const firstSyntheticMessage = Effect.fn("SessionHistory.firstSyntheticMessage")(function* ( - db: DatabaseService, - sessionID: SessionSchema.ID, -) { - const row = yield* db - .select() - .from(SessionMessageTable) - .where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "synthetic"))) - .orderBy(asc(SessionMessageTable.seq)) - .get() - .pipe(Effect.orDie) - if (!row) return undefined - const message = yield* decodeMessageRow(row).pipe(Effect.orElseSucceed(() => undefined)) - return message?.type === "synthetic" ? message : undefined -}) - export * as SessionHistory from "./history.js" diff --git a/packages/core/src/session/title.ts b/packages/core/src/session/title.ts index 18a4a8e7d6e..4ba54f2ede4 100644 --- a/packages/core/src/session/title.ts +++ b/packages/core/src/session/title.ts @@ -101,15 +101,14 @@ export const layer = Layer.effect( const session = yield* store.get(sessionID) if (!session) return const firstUser = yield* SessionHistory.firstUserMessage(db, session.id) - const firstPrompt = firstUser ?? (yield* SessionHistory.firstSyntheticMessage(db, session.id)) - if (!firstPrompt) return + if (!firstUser) return const text = !isUntitled(session) ? yield* store.context(session.id).pipe( Effect.map((messages) => { - const original = `Original request:\n${firstPrompt.text.slice(0, MAX_FIRST_MESSAGE_LENGTH)}` + const original = `Original request:\n${firstUser.text.slice(0, MAX_FIRST_MESSAGE_LENGTH)}` const recent = messages .flatMap((message) => { - if (message.type === "user" && message.id !== firstPrompt.id) return [`User: ${message.text.trim()}`] + if (message.type === "user" && message.id !== firstUser.id) return [`User: ${message.text.trim()}`] if (message.type !== "assistant") return [] const text = message.content .flatMap((part) => (part.type === "text" ? [part.text.trim()] : [])) @@ -122,9 +121,9 @@ export const layer = Layer.effect( const prefix = `${original}\n\nRecent conversation:\n` return `${prefix}${recent.slice(-(MAX_CONTEXT_LENGTH - prefix.length))}` }), - Effect.orElseSucceed(() => firstPrompt.text), + Effect.orElseSucceed(() => firstUser.text), ) - : firstPrompt.text + : firstUser.text const selection = yield* context.selectTitle(session) if (!selection) return const title = diff --git a/packages/core/test/plugin/goal.test.ts b/packages/core/test/plugin/goal.test.ts deleted file mode 100644 index 6038673459c..00000000000 --- a/packages/core/test/plugin/goal.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { describe, expect } from "bun:test" -import type { CommandDefinition } from "@opencode/plugin/effect/command" -import { Event } from "@opencode/schema/event" -import { Session } from "@opencode/schema/session" -import { SessionEvent } from "@opencode/schema/session-event" -import { SessionInbox } from "@opencode/schema/session-inbox" -import { SessionMessage } from "@opencode/schema/session-message" -import { DateTime, Deferred, Effect, PubSub, Stream } from "effect" -import { GoalPlugin } from "@opencode/core/plugin/goal" -import { it } from "../lib/effect" -import { host } from "./host" - -const sessionID = Session.ID.make("ses_goal_test") - -describe("GoalPlugin.Plugin", () => { - it.effect("continues a goal until evaluation reports completion", () => - Effect.gen(function* () { - const event: SessionEvent.Execution.Succeeded = { - id: Event.ID.create(), - created: 0, - durable: { aggregateID: sessionID, seq: Event.Seq.make(0), version: Event.Version.make(1) }, - type: "session.execution.succeeded", - data: { sessionID }, - } - const events = yield* PubSub.unbounded() - const completed = yield* Deferred.make() - const storage = new Map() - const descriptions = new Array() - let command: CommandDefinition | undefined - - yield* GoalPlugin.Plugin.effect( - host({ - command: { - list: () => Effect.die("unused command.list"), - reload: () => Effect.die("unused command.reload"), - transform: (callback) => { - callback({ add: (definition) => (command = definition) }) - return Effect.succeed({ dispose: Effect.void }) - }, - }, - event: { subscribe: () => Stream.fromPubSub(events) }, - storage: { - get: (key) => Effect.succeed(storage.get(key) as never), - set: (key, value) => Effect.sync(() => storage.set(key, value)), - remove: (key) => Effect.sync(() => storage.delete(key)), - scan: () => Effect.die("unused storage.scan"), - }, - session: { - generate: () => Effect.succeed({ text: "COMPLETE" }), - synthetic: (input) => - Effect.gen(function* () { - descriptions.push(input.description ?? "") - if (input.description === "Goal completed") yield* Deferred.succeed(completed, undefined) - return SessionInbox.Synthetic.make({ - id: SessionMessage.ID.create(), - sessionID: input.sessionID, - time: { created: DateTime.makeUnsafe(0) }, - type: "synthetic", - payload: { text: input.text, description: input.description }, - delivery: input.delivery ?? "steer", - }) - }), - }, - }), - ) - yield* Effect.yieldNow - if (!command) return yield* Effect.die("Goal command was not registered") - - yield* command.execute({ sessionID, prompt: { text: "Finish the task" }, delivery: "steer" }) - yield* PubSub.publish(events, event) - yield* Deferred.await(completed) - - expect(descriptions).toEqual(["Goal started: Finish the task", "Goal completed"]) - expect(storage.get(`session/${sessionID}/goal`)).toEqual({ goal: "Finish the task", active: false }) - }), - ) -})