mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
revert(core): remove autonomous goal command (#49866)
Co-authored-by: thdxr <826656+thdxr@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
681338c533
commit
4ee9bf0213
@@ -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,
|
||||
})
|
||||
}),
|
||||
})
|
||||
})
|
||||
}),
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 })
|
||||
}),
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user