feat(plan): richer PlanArtifact schema for v0.9.0 (#2691)#2733
feat(plan): richer PlanArtifact schema for v0.9.0 (#2691)#2733idling11 wants to merge 2 commits into
Conversation
Extend `update_plan` and `PlanState` with structured fields so Plan
mode produces a grounded, reviewable artifact instead of just a step
list. Backward-compatible: legacy callers sending only `plan` +
`explanation` still work.
New fields (all optional, all default to None/empty):
- `title`, `objective`, `context_summary`
- `sources_used`, `constraints` (Vec<String>)
- `recommended_approach`
- `verification_plan`, `risks_and_unknowns`
- `handoff_packet`
Changes:
- `crates/tui/src/tools/plan.rs`:
- Extend `UpdatePlanArgs` with 9 new optional fields + Default derive
- Extend `PlanState` to store all new fields
- Extend `PlanSnapshot` with skip-serializing-when-empty annotations
- Add accessor methods for all fields
- Update tool schema, description, and execute to parse new fields
- 6 new unit tests covering legacy compat, full artifact parse,
snapshot serialization, is_empty, and Default
- `crates/tui/src/prompts/modes/plan.md`:
- Prompt now asks for grounded artifact fields
- `crates/tui/src/tui/app.rs`, `crates/tui/src/commands/mod.rs`:
- Update test struct literals with `..Default::default()`
Closes Hmbown#2691
|
Thanks @idling11 for taking the time to contribute. This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this pull request is staying open. When enforcement is enabled, pull requests from contributors who are not listed in Please read |
There was a problem hiding this comment.
Code Review
This pull request introduces several new fields (such as title, objective, context_summary, sources_used, constraints, recommended_approach, verification_plan, risks_and_unknowns, and handoff_packet) to the structured implementation plan artifact (UpdatePlanArgs, PlanSnapshot, and PlanState) in crates/tui/src/tools/plan.rs. It also updates the tool's description, schema, documentation, and adds corresponding tests. The reviewer pointed out that the is_empty method in PlanState misses checking several of these newly added fields, which could cause a non-empty plan to be incorrectly reported as empty, and provided a suggestion to fix this.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| pub fn is_empty(&self) -> bool { | ||
| self.steps.is_empty() && self.explanation.as_deref().unwrap_or("").is_empty() | ||
| self.steps.is_empty() | ||
| && self.explanation.as_deref().unwrap_or("").is_empty() | ||
| && self.title.as_deref().unwrap_or("").is_empty() | ||
| && self.objective.as_deref().unwrap_or("").is_empty() | ||
| && self.context_summary.as_deref().unwrap_or("").is_empty() | ||
| && self | ||
| .recommended_approach | ||
| .as_deref() | ||
| .unwrap_or("") | ||
| .is_empty() | ||
| } |
There was a problem hiding this comment.
The is_empty method does not check several of the newly added fields, such as sources_used, constraints, verification_plan, risks_and_unknowns, and handoff_packet. If a plan contains only these fields, is_empty will incorrectly return true.
| pub fn is_empty(&self) -> bool { | |
| self.steps.is_empty() && self.explanation.as_deref().unwrap_or("").is_empty() | |
| self.steps.is_empty() | |
| && self.explanation.as_deref().unwrap_or("").is_empty() | |
| && self.title.as_deref().unwrap_or("").is_empty() | |
| && self.objective.as_deref().unwrap_or("").is_empty() | |
| && self.context_summary.as_deref().unwrap_or("").is_empty() | |
| && self | |
| .recommended_approach | |
| .as_deref() | |
| .unwrap_or("") | |
| .is_empty() | |
| } | |
| pub fn is_empty(&self) -> bool { | |
| self.steps.is_empty() | |
| && self.explanation.as_deref().unwrap_or("").is_empty() | |
| && self.title.as_deref().unwrap_or("").is_empty() | |
| && self.objective.as_deref().unwrap_or("").is_empty() | |
| && self.context_summary.as_deref().unwrap_or("").is_empty() | |
| && self.recommended_approach.as_deref().unwrap_or("").is_empty() | |
| && self.sources_used.is_empty() | |
| && self.constraints.is_empty() | |
| && self.verification_plan.as_deref().unwrap_or("").is_empty() | |
| && self.risks_and_unknowns.as_deref().unwrap_or("").is_empty() | |
| && self.handoff_packet.as_deref().unwrap_or("").is_empty() | |
| } |
|
brilliant - thank you!! |
18b1f3e to
546265a
Compare
| "required": [ | ||
| "title", | ||
| "objective", | ||
| "context_summary", | ||
| "sources_used", | ||
| "constraints", | ||
| "recommended_approach", | ||
| "plan", | ||
| "verification_plan", | ||
| "risks_and_unknowns", | ||
| "handoff_packet" | ||
| ] |
There was a problem hiding this comment.
The
required array in the JSON schema lists all 9 new fields as mandatory, directly contradicting the tool description ("All fields except plan are optional"), the plan.md prompt ("All fields except plan are optional"), and the PR's backward-compatibility claim. Because LLMs treat required as binding, the model will attempt to populate every field on every call — even when it has nothing meaningful to say — rather than only including what was actually discovered. A caller that sends only plan + explanation would also violate this schema, breaking the promised legacy path.
| "required": [ | |
| "title", | |
| "objective", | |
| "context_summary", | |
| "sources_used", | |
| "constraints", | |
| "recommended_approach", | |
| "plan", | |
| "verification_plan", | |
| "risks_and_unknowns", | |
| "handoff_packet" | |
| ] | |
| "required": [ | |
| "plan" | |
| ] |
Extend
update_planandPlanStatewith structured fields so Plan mode produces a grounded, reviewable artifact instead of just a step list. Backward-compatible: legacy callers sending onlyplan+explanationstill work.New fields (all optional, all default to None/empty):
title,objective,context_summarysources_used,constraints(Vec)recommended_approachverification_plan,risks_and_unknownshandoff_packetChanges:
crates/tui/src/tools/plan.rs:UpdatePlanArgswith 9 new optional fields + Default derivePlanStateto store all new fieldsPlanSnapshotwith skip-serializing-when-empty annotationscrates/tui/src/prompts/modes/plan.md:crates/tui/src/tui/app.rs,crates/tui/src/commands/mod.rs:..Default::default()Closes #2691
Summary
Testing
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-featurescargo test --workspace --all-featuresChecklist
Greptile Summary
This PR extends
update_planandPlanStatewith nine new optional fields (title,objective,context_summary,sources_used,constraints,recommended_approach,verification_plan,risks_and_unknowns,handoff_packet) to produce a richer, reviewable plan artifact instead of just a step list.plan.rs:UpdatePlanArgs,PlanState, andPlanSnapshotare all extended with the new fields;is_empty(),update(),snapshot(), and the accessor methods are correctly updated; six new unit tests cover legacy compat, full artifact parsing, snapshot serialization, andDefault.plan.md: Prompt is updated to describe the new fields and correctly marks all butplanas optional.commands/mod.rsandapp.rs: Test struct literals are updated with..Default::default()to compile against the expanded struct.Confidence Score: 4/5
Safe to merge after fixing the required-fields mismatch in the JSON schema.
The input_schema() declares all nine new fields as required, but both the tool description and plan.md state they are optional, and the execute path treats them as optional. A model strictly following the schema will feel compelled to populate every field on every invocation rather than including only what was discovered, defeating the design intent and breaking the stated legacy compatibility path.
crates/tui/src/tools/plan.rs — the required array in input_schema() needs to be reduced to ["plan"].
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[LLM calls update_plan] --> B{Schema required check} B -->|Schema lists ALL 10 fields as required| C[LLM populates every field] B -->|Description and plan.md say only plan is required| D[LLM includes only discovered fields] C --> E[execute: opt_str / opt_strs parse input] D --> E E --> F[UpdatePlanArgs all new fields are Option or Vec] F --> G[PlanState.update] G --> H[Optional string fields stored as Option] G --> I[sources_used and constraints stored as Vec] H --> J[PlanState.snapshot] I --> J J --> K[PlanSnapshot with skip_serializing_if annotations] K --> L[Sidebar display] K --> M[status command only reads explanation and items]Comments Outside Diff (1)
crates/tui/src/commands/mod.rs, line 855-865 (link)This block still only reads
snapshot.explanationandsnapshot.items. The new fields (title,objective,sources_used,constraints,recommended_approach,verification_plan,risks_and_unknowns,handoff_packet) are silently dropped when this status output is produced. If the status command is used as context during an Agent-mode handoff or review, none of the richer artifact data will appear.Reviews (3): Last reviewed commit: "fix(plan): check all new fields in PlanS..." | Re-trigger Greptile