@@ -316,8 +316,64 @@ func (r *REPL) executeToolNative(toolName string, args ...string) (string, error
316316 return out .String (), nil
317317}
318318
319+ // planList is []string but tolerates LLMs that emit plan entries as objects,
320+ // numbers, or other non-string JSON values. Non-string elements are preserved
321+ // as their compact JSON encoding so downstream code can keep treating plan
322+ // entries as strings.
323+ type planList []string
324+
325+ func (p * planList ) UnmarshalJSON (data []byte ) error {
326+ trimmed := bytes .TrimSpace (data )
327+ if len (trimmed ) == 0 || bytes .Equal (trimmed , []byte ("null" )) {
328+ * p = nil
329+ return nil
330+ }
331+ // Accept a single string or object as a one-element plan.
332+ if trimmed [0 ] != '[' {
333+ var one any
334+ if err := json .Unmarshal (data , & one ); err != nil {
335+ return err
336+ }
337+ * p = planList {planEntryToString (one )}
338+ return nil
339+ }
340+ var raw []json.RawMessage
341+ if err := json .Unmarshal (data , & raw ); err != nil {
342+ return err
343+ }
344+ out := make (planList , 0 , len (raw ))
345+ for _ , item := range raw {
346+ var s string
347+ if err := json .Unmarshal (item , & s ); err == nil {
348+ out = append (out , s )
349+ continue
350+ }
351+ var v any
352+ if err := json .Unmarshal (item , & v ); err != nil {
353+ return err
354+ }
355+ out = append (out , planEntryToString (v ))
356+ }
357+ * p = out
358+ return nil
359+ }
360+
361+ func planEntryToString (v any ) string {
362+ switch t := v .(type ) {
363+ case string :
364+ return t
365+ case nil :
366+ return ""
367+ }
368+ b , err := json .Marshal (v )
369+ if err != nil {
370+ return fmt .Sprintf ("%v" , v )
371+ }
372+ return string (b )
373+ }
374+
319375type PlanResponse struct {
320- Plan [] string `json:"plan"`
376+ Plan planList `json:"plan"`
321377 CurrentPlanIndex int `json:"current_plan_index"`
322378 Progress string `json:"progress"`
323379 NextStep string `json:"next_step"`
0 commit comments