@@ -14,7 +14,9 @@ import (
1414
1515 "github.com/BackendStack21/kode"
1616 "github.com/BackendStack21/kode/internal/config"
17+ "github.com/BackendStack21/kode/internal/llm"
1718 "github.com/BackendStack21/kode/internal/render"
19+ "github.com/BackendStack21/kode/internal/session"
1820)
1921
2022// version is set at build time via ldflags: -ldflags "-X main.version=v0.2.1"
@@ -87,6 +89,16 @@ func main() {
8789 fmt .Fprintf (os .Stderr , "kode: %v\n " , err )
8890 os .Exit (1 )
8991 }
92+ case "continue" :
93+ if err := continueCmd (os .Args [2 :]); err != nil {
94+ fmt .Fprintf (os .Stderr , "kode: %v\n " , err )
95+ os .Exit (1 )
96+ }
97+ case "session" :
98+ if err := sessionCmd (os .Args [2 :]); err != nil {
99+ fmt .Fprintf (os .Stderr , "kode: %v\n " , err )
100+ os .Exit (1 )
101+ }
90102 default :
91103 fmt .Fprintf (os .Stderr , "kode: unknown command %q\n " , os .Args [1 ])
92104 printUsage ()
@@ -114,6 +126,7 @@ type runFlags struct {
114126 Sandbox * bool // nil = not set
115127 NoColor * bool // nil = not set
116128 NoAgents * bool // nil = not set
129+ Session * bool // nil = not set; true = save session after run
117130 Task string
118131
119132 // Sandbox-specific CLI flags
@@ -161,6 +174,9 @@ func parseRunFlags(args []string) (runFlags, error) {
161174 case "--no-agents" :
162175 f .NoAgents = boolPtr (true )
163176 i ++
177+ case "--session" :
178+ f .Session = boolPtr (true )
179+ i ++
164180 case "--sandbox-image" :
165181 f .SandboxImage = args [i + 1 ]
166182 i += 2
@@ -195,11 +211,17 @@ done:
195211func printUsage () {
196212 fmt .Println (`Usage:
197213 kode run [flags] <task>
214+ kode run --session [flags] <task>
215+ kode continue [--id <id>] <task>
216+ kode session <list|show [id]|delete <id>>
198217 kode init [--global | -g] [--force | -f]
199218 kode version
200219
201220Commands:
202221 run Execute a task with the agent loop
222+ run --session Execute and save conversation as a session
223+ continue Continue the most recent session (or by --id)
224+ session Manage sessions: list, show, delete
203225 init Create a config file (default: ./kode.json)
204226 version Print version and exit
205227
@@ -218,6 +240,7 @@ Run flags:
218240 Empty = profile default = provider default.
219241 --no-color Disable colored terminal output
220242 --no-agents Skip loading AGENTS.md from working directory
243+ --session Save conversation as a multi-turn session
221244 --system <prompt> System prompt override
222245
223246Sandbox flags:
@@ -532,11 +555,38 @@ func run(args []string) error {
532555 defer cancel ()
533556
534557 rend .Start (f .Task )
535- result , err := agent .Run (ctx , f .Task )
536- if err != nil {
537- return err
558+
559+ if f .Session != nil && * f .Session {
560+ // Multi-turn session mode: save conversation history
561+ messages := []llm.Message {
562+ {Role : "user" , Content : f .Task },
563+ }
564+ if systemMessage != "" {
565+ messages = append ([]llm.Message {{Role : "system" , Content : systemMessage }}, messages ... )
566+ }
567+
568+ result , allMessages , err := agent .RunWithMessages (ctx , messages )
569+ if err != nil {
570+ return err
571+ }
572+ _ = result
573+
574+ store , err := session .NewStore ()
575+ if err != nil {
576+ return fmt .Errorf ("session store: %w" , err )
577+ }
578+ sess , err := store .Create (allMessages , resolved .Model , f .Task )
579+ if err != nil {
580+ return fmt .Errorf ("save session: %w" , err )
581+ }
582+ fmt .Fprintf (os .Stderr , "kode: session %s saved — continue with: kode continue \" ...\" \n " , sess .ID )
583+ } else {
584+ // Single-shot mode (default)
585+ _ , err := agent .Run (ctx , f .Task )
586+ if err != nil {
587+ return err
588+ }
538589 }
539- _ = result // rendered by the loop engine via Renderer
540590 return nil
541591}
542592
@@ -684,3 +734,239 @@ func getVersion() string {
684734 }
685735 return "dev"
686736}
737+
738+ // ── Continue (Multi-Turn) ─────────────────────────────────────────────
739+
740+ // continueCmd handles `kode continue [--id <id>] <task>`.
741+ // It loads an existing session (latest or by ID), appends the new task,
742+ // runs the agent with full history, and saves the updated session.
743+ func continueCmd (args []string ) error {
744+ sessionID := ""
745+ i := 0
746+ for i < len (args )- 1 && args [i ] == "--id" {
747+ sessionID = args [i + 1 ]
748+ i += 2
749+ }
750+ if i >= len (args ) {
751+ return fmt .Errorf ("no task provided for continue" )
752+ }
753+ task := strings .Join (args [i :], " " )
754+
755+ store , err := session .NewStore ()
756+ if err != nil {
757+ return fmt .Errorf ("session store: %w" , err )
758+ }
759+
760+ var sess * session.Session
761+ if sessionID != "" {
762+ sess , err = store .Load (sessionID )
763+ } else {
764+ sess , err = store .Latest ()
765+ }
766+ if err != nil {
767+ return fmt .Errorf ("load session: %w" , err )
768+ }
769+
770+ fmt .Fprintf (os .Stderr , "kode: continuing session %s (turn %d → %d)\n " ,
771+ sess .ID , sess .Turns , sess .Turns + 1 )
772+
773+ // Build tools
774+ tools := builtinTools ()
775+ var sandboxCleanup func () error
776+
777+ // Resolve config (no CLI flags for continue — uses session's model)
778+ resolved := config .LoadConfig (config.CLIFlags {Model : sess .Model })
779+
780+ systemMessage := resolved .System
781+ if systemMessage == "" {
782+ systemMessage = defaultSystem
783+ }
784+
785+ // Sandbox (if enabled in config)
786+ if resolved .Sandbox {
787+ sbCfg := sandboxConfig {
788+ Image : resolved .SandboxImage ,
789+ Network : resolved .SandboxNetwork ,
790+ Readonly : resolved .SandboxReadonly ,
791+ Memory : resolved .SandboxMemory ,
792+ CPUs : resolved .SandboxCPUs ,
793+ User : resolved .SandboxUser ,
794+ }
795+ cleanup , err := setupSandbox (tools , sbCfg )
796+ if err != nil {
797+ return fmt .Errorf ("sandbox: %w" , err )
798+ }
799+ sandboxCleanup = cleanup
800+ }
801+
802+ // Renderer
803+ modelLabel := kode .ProfileLabel (resolved .Model )
804+ if modelLabel == "" {
805+ modelLabel = "deepseek-chat"
806+ }
807+ color := ! resolved .NoColor && render .ColorEnabled ()
808+ rend := render .New (os .Stderr , color ).WithModel (modelLabel )
809+
810+ agent , err := kode .New (kode.Config {
811+ Model : resolved .Model ,
812+ BaseURL : resolved .BaseURL ,
813+ APIKey : resolved .APIKey ,
814+ MaxIterations : resolved .MaxIter ,
815+ SystemMessage : systemMessage ,
816+ NoProjectFile : resolved .NoAgents ,
817+ Thinking : resolved .Thinking ,
818+ Tools : tools ,
819+ SandboxCleanup : sandboxCleanup ,
820+ Renderer : rend ,
821+ })
822+ if err != nil {
823+ return err
824+ }
825+ defer agent .Close ()
826+
827+ // Build message history: session messages + new user message
828+ // The system message is already in the session
829+ messages := sess .GetMessages ()
830+ messages = append (messages , llm.Message {Role : "user" , Content : task })
831+
832+ ctx , cancel := signal .NotifyContext (context .Background (), os .Interrupt )
833+ defer cancel ()
834+
835+ rend .Start (task )
836+ result , allMessages , err := agent .RunWithMessages (ctx , messages )
837+ if err != nil {
838+ return err
839+ }
840+ _ = result
841+
842+ // Save updated session
843+ if err := store .Append (sess .ID , allMessages [len (sess .GetMessages ()):]); err != nil {
844+ return fmt .Errorf ("save session: %w" , err )
845+ }
846+
847+ fmt .Fprintf (os .Stderr , "kode: session %s saved (%d turns)\n " , sess .ID , sess .Turns + 1 )
848+ return nil
849+ }
850+
851+ // ── Session Management ────────────────────────────────────────────────
852+
853+ // sessionCmd handles `kode session <list|show|delete> [args]`.
854+ func sessionCmd (args []string ) error {
855+ if len (args ) == 0 {
856+ fmt .Fprintf (os .Stderr , "Usage: kode session <list|show [id]|delete <id>>\n " )
857+ return nil
858+ }
859+
860+ store , err := session .NewStore ()
861+ if err != nil {
862+ return fmt .Errorf ("session store: %w" , err )
863+ }
864+
865+ switch args [0 ] {
866+ case "list" :
867+ return listSessions (store )
868+ case "show" :
869+ return showSession (store , args [1 :])
870+ case "delete" :
871+ return deleteSession (store , args [1 :])
872+ default :
873+ return fmt .Errorf ("unknown session command %q (use list, show, delete)" , args [0 ])
874+ }
875+ }
876+
877+ func listSessions (store * session.Store ) error {
878+ sessions , err := store .List (20 )
879+ if err != nil {
880+ return fmt .Errorf ("list sessions: %w" , err )
881+ }
882+ if len (sessions ) == 0 {
883+ fmt .Println ("No sessions found." )
884+ return nil
885+ }
886+
887+ fmt .Printf ("%-22s %-5s %-30s %s\n " , "ID" , "Turns" , "Model" , "Task" )
888+ fmt .Println (strings .Repeat ("─" , 80 ))
889+ for _ , s := range sessions {
890+ task := shorten (s .Task , 30 )
891+ model := shorten (s .Model , 20 )
892+ fmt .Printf ("%-22s %-5d %-30s %s\n " , s .ID , s .Turns , model , task )
893+ }
894+ return nil
895+ }
896+
897+ func showSession (store * session.Store , args []string ) error {
898+ var id string
899+ if len (args ) > 0 {
900+ id = args [0 ]
901+ } else {
902+ sess , err := store .Latest ()
903+ if err != nil {
904+ return fmt .Errorf ("no sessions found: %w" , err )
905+ }
906+ id = sess .ID
907+ }
908+
909+ sess , err := store .Load (id )
910+ if err != nil {
911+ return fmt .Errorf ("load session: %w" , err )
912+ }
913+
914+ fmt .Printf ("Session: %s\n " , sess .ID )
915+ fmt .Printf ("Model: %s\n " , sess .Model )
916+ fmt .Printf ("Turns: %d\n " , sess .Turns )
917+ fmt .Printf ("Created: %s\n " , sess .CreatedAt .Format ("2006-01-02 15:04:05 UTC" ))
918+ fmt .Printf ("Updated: %s\n " , sess .UpdatedAt .Format ("2006-01-02 15:04:05 UTC" ))
919+ fmt .Printf ("Task: %s\n " , sess .Task )
920+ fmt .Println ()
921+
922+ for i , msg := range sess .Messages {
923+ content := strings .TrimSpace (msg .Content )
924+ switch msg .Role {
925+ case "system" :
926+ fmt .Printf ("── [SYSTEM] ──\n %s\n \n " , content )
927+ case "user" :
928+ fmt .Printf ("── [USER Turn %d] ──\n %s\n \n " , countUserTurnsUpTo (sess .Messages , i ), content )
929+ case "assistant" :
930+ if len (msg .ToolCalls ) > 0 {
931+ for _ , tc := range msg .ToolCalls {
932+ fmt .Printf ("── [TOOL CALL: %s] ──\n %s\n \n " , tc .Function .Name , tc .Function .Arguments )
933+ }
934+ } else {
935+ fmt .Printf ("── [ASSISTANT] ──\n %s\n \n " , content )
936+ }
937+ case "tool" :
938+ fmt .Printf ("── [TOOL RESULT: %s] ──\n %s\n \n " , msg .Name , shorten (content , 200 ))
939+ }
940+ }
941+ return nil
942+ }
943+
944+ func deleteSession (store * session.Store , args []string ) error {
945+ if len (args ) == 0 {
946+ return fmt .Errorf ("usage: kode session delete <id>" )
947+ }
948+ if err := store .Delete (args [0 ]); err != nil {
949+ return fmt .Errorf ("delete session: %w" , err )
950+ }
951+ fmt .Printf ("Deleted session %s\n " , args [0 ])
952+ return nil
953+ }
954+
955+ // countUserTurnsUpTo counts user messages up to (but not including) index n.
956+ func countUserTurnsUpTo (messages []llm.Message , n int ) int {
957+ count := 0
958+ for i := 0 ; i < n && i < len (messages ); i ++ {
959+ if messages [i ].Role == "user" {
960+ count ++
961+ }
962+ }
963+ return count
964+ }
965+
966+ // shorten truncates s to n chars, adding "…" if truncated.
967+ func shorten (s string , n int ) string {
968+ if len (s ) <= n {
969+ return s
970+ }
971+ return s [:n ] + "…"
972+ }
0 commit comments