feat: add native /goal command for autonomous task completion#28610
feat: add native /goal command for autonomous task completion#28610NathanKong76 wants to merge 8 commits 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": () => {}, |
e4e3f9e to
3d912fc
Compare
Add a native /goal command that enables multi-turn autonomous goal execution, matching the implementation approach of Codex CLI. Key changes: - New GoalTable schema (thread_id PK, objective, status, token budget, iteration count) and GoalStatus union type in session.sql.ts - Goal runtime state machine in session/goal.ts with CRUD operations, continuation prompt generation, budget tracking, and iteration limits - create_goal and update_goal model tools in tool/goal.ts, with SessionGoal.Service resolved lazily inside execute (not at define time) - Goal tools registered unconditionally in tool/registry.ts, feature gated only at runtime via experimental.goals config check - /goal command registered in command/index.ts behind experimental.goals feature flag - Goal continuation logic in prompt.ts runLoop: after outcome is 'break', checks for active goal and injects continuation prompt as a synthetic user message before continuing the loop - SessionGoal.defaultLayer merged into AppLayer in app-runtime.ts - Goal event types (GoalSet, GoalUpdated, GoalCleared, GoalCompleted) in session-event.ts with no-op handlers in session-message-updater.ts Fixes anomalyco#27162
3d912fc to
33076c5
Compare
…tation, add get_goal tool
|
@copilot review |
|
Hi @adamdotdevin, all Copilot review comments have been addressed. I have also added additional features on top: |
|
Quick update: I found and fixed a critical missing piece — the database migration for the |
Issue for this PR
Closes #27167
Type of change
What does this PR do?
Adds a native
/goalcommand that enables multi-turn autonomous goal execution, similar to Codex CLI's/goalcommand. Feature gated behindexperimental.goalsconfig flag (default off).Implementation approach:
The goal runtime is backed by a new
GoalTablein SQLite (via existing Drizzle ORM). ASessionGoal.Servicestate machine handles CRUD, token budget tracking, and iteration limits. Two model tools (create_goal,update_goal) let the model manage goals autonomously.The key design challenge was avoiding
InstanceRef not providederrors —config.get()needsInstanceRefwhich isn't available during layer resolution. The fix:SessionGoal.Serviceandconfig.get()are accessed lazily insiderunLoop(afteroutcome === "break"), not at the layer effect level.Goal continuation works by injecting a synthetic user message with a continuation prompt when the goal is still active after a turn. The loop continues automatically until the goal is marked complete or the iteration limit (default 50) is reached.
/goalcommand registration is conditional oncfg.experimental?.goals === true.How did you verify your code works?
typecheck: cleantest/tool/registry.test.ts: 15/15 passtest/session/prompt.test.ts: 37/3 (3 pre-existing failures unrelated to this change — confirmed same failures exist ondevbranch)Screenshots / recordings
N/A — CLI change only.
Checklist