dodo: save config tool

Change-Id: Ia8c8639cda093a3776ff018c2b77e63449cbc78c
diff --git a/dodo_tools/dodo.go b/dodo_tools/dodo.go
index 41127df..fdb7c8f 100644
--- a/dodo_tools/dodo.go
+++ b/dodo_tools/dodo.go
@@ -1,8 +1,8 @@
 package dodo_tools
 
 import (
-	"context"
 	"bytes"
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -16,7 +16,6 @@
 	projectId      string
 }
 
-
 func NewGetProjectConfigTool(apiBaseAddress string, projectId string) *llm.Tool {
 	tool := &GetProjectConfigTool{
 		apiBaseAddress: apiBaseAddress,
@@ -99,11 +98,10 @@
 
 type ValidateConfigOutput struct {
 	Success bool `json:"success"`
-	Errors  any `json:"errors,omitempty"`
+	Errors  any  `json:"errors,omitempty"`
 }
 
 func (d *ValidateConfigTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
-	fmt.Printf("%s\n", string(m))
 	var input ValidateConfigInput
 	if err := json.Unmarshal(m, &input); err != nil {
 		return nil, err
@@ -171,7 +169,7 @@
 
 type DeployProjectOutput struct {
 	Success bool `json:"success"`
-	Errors  any `json:"errors,omitempty"`
+	Errors  any  `json:"errors,omitempty"`
 }
 
 type deployProjectReq struct {
@@ -187,7 +185,6 @@
 	if err := json.Unmarshal([]byte(input.Config), &req.Config); err != nil {
 		return nil, err
 	}
-	fmt.Printf("### %+v\n", req)
 	jsonReq, err := json.Marshal(req)
 	if err != nil {
 		return nil, err
@@ -207,10 +204,91 @@
 	return llm.TextContent(string("Project deployed successfully")), nil
 }
 
+// Save
+
+type SaveProjectTool struct {
+	apiBaseAddress string
+	projectId      string
+}
+
+const (
+	saveProjectSchemaInputSchema = `
+{
+	"type": "object",
+	"properties": {
+		"config": {
+			"type": "string",
+			"description": "Serialized dodo-app configuration to save"
+		}
+	}
+}
+`
+)
+
+func NewSaveProjectTool(apiBaseAddress string, projectId string) *llm.Tool {
+	tool := &SaveProjectTool{
+		apiBaseAddress: apiBaseAddress,
+		projectId:      projectId,
+	}
+
+	return &llm.Tool{
+		Name:        "dodo_save_config",
+		Description: "A tool for saving the dodo-app configuration",
+		InputSchema: llm.MustSchema(saveProjectSchemaInputSchema),
+		Run:         tool.Run,
+		EndsTurn:    true,
+	}
+}
+
+type SaveProjectInput struct {
+	Config string `json:"config"`
+}
+
+type SaveProjectOutput struct {
+	Success bool `json:"success"`
+	Errors  any  `json:"errors,omitempty"`
+}
+
+type saveProjectReq struct {
+	Type   string         `json:"type"`
+	Config map[string]any `json:"config"`
+}
+
+func (d *SaveProjectTool) Run(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+	var input SaveProjectInput
+	if err := json.Unmarshal(m, &input); err != nil {
+		return nil, err
+	}
+	req := saveProjectReq{
+		Type: "config",
+	}
+	if err := json.Unmarshal([]byte(input.Config), &req.Config); err != nil {
+		return nil, err
+	}
+	jsonReq, err := json.Marshal(req)
+	if err != nil {
+		return nil, err
+	}
+	resp, err := http.Post(fmt.Sprintf("%s/api/project/%s/saved", d.apiBaseAddress, d.projectId), "application/json", bytes.NewBuffer(jsonReq))
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("failed to save project: %s", string(body))
+	}
+	return llm.TextContent(string("Project saved successfully")), nil
+}
+
 func NewDodoTools(apiBaseAddress string, projectId string) []*llm.Tool {
 	return []*llm.Tool{
 		NewGetProjectConfigTool(apiBaseAddress, projectId),
 		NewValidateConfigTool(apiBaseAddress),
 		NewDeployProjectTool(apiBaseAddress, projectId),
+		NewSaveProjectTool(apiBaseAddress, projectId),
 	}
-}
\ No newline at end of file
+}
diff --git a/loop/agent_system_prompt.txt b/loop/agent_system_prompt.txt
index 840fcde..473431d 100644
--- a/loop/agent_system_prompt.txt
+++ b/loop/agent_system_prompt.txt
@@ -83,6 +83,7 @@
 Use following tools to interact with dodo:
 1. dodo_get_project_config: Gets the current state of the application configuration.
 2. dodo_validate_config: Takes dodo-app configuration and validates it. Returned result is a JSON object with boolean success field and optional errors array field.
+3. dodo_save_config: Takes dodo-app configuration and saves it as a current working draft. Always use this tool before actually deploying the new configuration, so that user can verify the changes first.
 3. dodo_deploy_project: Takes new configuration and deployes it.
 
 You might want to use dodo tools in following scenarios:
@@ -90,8 +91,11 @@
 2. User explicitely asks to add new infrastructure pieces or modify existing ones.
 3. User asks you to implement new feature which requires new infrastucture piece.
 
-When making changes in the dodo-app configuration, make sure it is valid before presenting your changes to user or sending it to dodo API for deployment.
-When validating inspect success and error fields and fix all errors. Use the todo_read and todo_write tools to organize and track your dodo-app configuration changes.
+When making changes in the dodo-app configuration, make sure that:
+1. It is valid before presenting your changes to user or sending it to dodo API for deployment.
+2. Carefully inspect validation result and fix all the errors.
+3. Make sure to save the new configuration before deploying it.
+4. Use the todo_read and todo_write tools to organize and track your dodo-app configuration changes.
 
 Always pretty print dodo-app config before presenting it to the user.