revert(core): remove autonomous goal command (#49866)

Co-authored-by: thdxr <826656+thdxr@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-09-18 23:14:36 +00:00
committed by GitHub
Unverified
parent 681338c533
commit 4ee9bf0213
5 changed files with 5 additions and 204 deletions
-102
View File
@@ -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<object>()
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 <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,
})
}),
})
})
}),
})
-2
View File
@@ -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,
-17
View File
@@ -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"
+5 -6
View File
@@ -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 =
-77
View File
@@ -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<typeof event>()
const completed = yield* Deferred.make<void>()
const storage = new Map<string, unknown>()
const descriptions = new Array<string>()
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 })
}),
)
})