feat: add /goal plugin for autonomous task completion#28610
feat: add /goal plugin for autonomous task completion#28610NathanKong76 wants to merge 1 commit into
Conversation
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Introduces an experimental “goals” capability that lets a session persist an objective, continue autonomously across turns, and update goal status via new tools and a /goal command.
Changes:
- Added
goalsession storage + service layer and wired it into the app/runtime layers. - Introduced
create_goal/update_goaltools and a new/goalcommand template. - Extended the session prompt loop to optionally auto-continue when a goal is active.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/opencode/src/tool/registry.ts | Registers goal tools and provides the goal service layer. |
| packages/opencode/src/tool/goal.ts | Implements create_goal and update_goal tools. |
| packages/opencode/src/session/session.sql.ts | Adds GoalStatus and a new goal SQLite table. |
| packages/opencode/src/session/prompt.ts | Adds experimental auto-continue behavior based on active goals. |
| packages/opencode/src/session/goal.ts | New SessionGoal service for CRUD + iteration/token tracking. |
| packages/opencode/src/effect/app-runtime.ts | Provides SessionGoal layer at the app level. |
| packages/opencode/src/config/config.ts | Adds experimental.goals config flag. |
| packages/opencode/src/command/template/goal.txt | Adds /goal command prompt template. |
| packages/opencode/src/command/index.ts | Registers /goal command when experimental.goals is enabled. |
| packages/core/src/session-message-updater.ts | Adds no-op handlers for new goal events. |
| packages/core/src/session-event.ts | Defines new goal-related session events and includes them in the union. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (outcome === "break") { | ||
| const goalSvc = yield* SessionGoal.Service | ||
| const configSvc = yield* Config.Service | ||
| const cfg = yield* configSvc.get() | ||
| const goalsEnabled = cfg.experimental?.goals === true | ||
| if (goalsEnabled) { | ||
| const goal = yield* goalSvc.get(sessionID) | ||
| if (goal && goalSvc.isGoalContinueNeeded(goal)) { | ||
| yield* goalSvc.incrementIteration(sessionID) | ||
| const updatedGoal = yield* goalSvc.get(sessionID) | ||
| if (updatedGoal && goalSvc.isGoalContinueNeeded(updatedGoal)) { | ||
| const contPrompt = updatedGoal.status === "budget_limited" | ||
| ? goalSvc.getBudgetLimitPrompt(updatedGoal) | ||
| : goalSvc.getContinuationPrompt(updatedGoal) | ||
| const msgId = MessageID.ascending() |
| }) | ||
|
|
||
| const isGoalContinueNeeded: Interface["isGoalContinueNeeded"] = (state: GoalState) => { | ||
| return state.status === "active" |
| }) | ||
|
|
||
| const runLoop: (sessionID: SessionID) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.run")( | ||
| const runLoop: (sessionID: SessionID) => Effect.Effect<MessageV2.WithParts, never, SessionGoal.Service | Config.Service> = Effect.fn("SessionPrompt.run")( |
| input: LoopInput, | ||
| ) { | ||
| return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID)) | ||
| return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID) as Effect.Effect<MessageV2.WithParts>) |
| const UpdateGoalParams = Schema.Struct({ | ||
| status: Schema.String.annotate({ | ||
| description: 'New status: "complete" (goal achieved), "blocked" (cannot proceed)', | ||
| }), | ||
| }) |
| @@ -0,0 +1,230 @@ | |||
| import { Effect, Layer, Context, Schema } from "effect" | |||
| import * as Log from "@opencode-ai/core/util/log" | ||
|
|
||
| const DEFAULT_MAX_ITERATIONS = 50 | ||
| const log = Log.create({ service: "session.goal" }) |
| timeUpdated: number | ||
| } | ||
|
|
||
| export class GoalNotFoundError extends Error { |
| } | ||
| } | ||
|
|
||
| export class GoalAlreadyExistsError extends Error { |
| "session.next.goal.set": () => {}, | ||
| "session.next.goal.updated": () => {}, | ||
| "session.next.goal.cleared": () => {}, | ||
| "session.next.goal.completed": () => {}, |
3d912fc to
33076c5
Compare
4728f09 to
b2c696b
Compare
|
Rewritten as a plugin per @aiden Cline feedback — zero core code changes, just 2 files. Architecture
Design
Open to feedback — happy to adjust anything. If this pattern works, future goal features (time limits, subtask goals, etc.) can be added to the plugin without touching core. |
7037767 to
0030ef4
Compare
A built-in plugin that enables multi-turn autonomous goal execution: - Tools: create_goal, update_goal, get_goal - Commands: /goal pause|resume|clear, --token-budget <N> - Continuation: listens to session.status idle event, checks active goal via .opencode/goals.json, injects <system-reminder> prompt via client.prompt() to trigger next LLM turn - Storage: single goals.json per directory, atomic write (tmp+rename) - Budget: iteration-based estimation, auto-transitions to budget_limited - Guards: 30s timeout on client.prompt(), inFlight set prevents duplicates - Zero core schema changes: all data in the plugin's own json file
0030ef4 to
1268d72
Compare
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
Issue for this PR
Closes #27167
Type of change
What does this PR do?
Adds a built-in plugin that enables multi-turn autonomous goal execution via
/goalcommand. The plugin hooks into opencode's existing plugin system — no core code changes.State is stored in
.opencode/goals.json(atomic write via tmp+rename), not in the SQLite database.How the continuation loop works:
session.statusidle event viaHooks.eventgoals.json— if goal is still active, injects a<system-reminder>continuation prompt viaclient.promptAsync()Commands:
/goal fix the bug— sets a new goal and sends template to LLM/goal pause//goal resume//goal clear— subcommands handled inline, no LLM callTools registered via
Hooks.tool:create_goal— create or reset a goalupdate_goal— mark as complete or blockedget_goal— read current goal stateNo token budget tracking — the model API doesn't expose per-turn token usage to plugins. The continuation prompt shows iteration count and elapsed time instead.
How did you verify your code works?
bun test test/plugin/goal.test.ts: 18/18 pass (storage, status logic, prompt generation, formatting)bun test test/plugin/goal.integration.test.ts: 16/16 pass (all hooks exercised via mock PluginInput: event continuation triggering, tool CRUD, command handling, edge cases)bun test test/session/prompt.test.ts: 38/13/3 (existing tests unaffected, 3 pre-existing shell timeout)bun test test/tool/registry.test.ts: 14/14 passbun typecheck: 0 errorsScreenshots / recordings
N/A — CLI change only.
Checklist