Skip to content

feat(aider): Add Coder Tasks and AgentAPI support #253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions aider-test-template/aider/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { describe, expect, it } from "bun:test";
import {
findResourceInstance,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";

describe("aider", async () => {
await runTerraformInit(import.meta.dir);

testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});

it("configures task prompt correctly", async () => {
const testPrompt = "Add a hello world function";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
task_prompt: testPrompt,
});

const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(
`This is your current task: ${testPrompt}`,
);
expect(instance.script).toContain("aider --architect --yes-always");
});

it("handles custom system prompt", async () => {
const customPrompt = "Report all tasks with state: working";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
system_prompt: customPrompt,
});

const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(customPrompt);
});

it("handles pre and post install scripts", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
experiment_pre_install_script: "echo 'Pre-install script executed'",
experiment_post_install_script: "echo 'Post-install script executed'",
});

const instance = findResourceInstance(state, "coder_script");

expect(instance.script).toContain("Running pre-install script");
expect(instance.script).toContain("Running post-install script");
expect(instance.script).toContain("base64 -d > /tmp/pre_install.sh");
expect(instance.script).toContain("base64 -d > /tmp/post_install.sh");
});

it("validates that use_screen and use_tmux cannot both be true", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
use_screen: true,
use_tmux: true,
});

const instance = findResourceInstance(state, "coder_script");

expect(instance.script).toContain(
"Error: Both use_screen and use_tmux cannot be enabled at the same time",
);
expect(instance.script).toContain("exit 1");
});

it("configures Aider with known provider and model", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
ai_provider: "anthropic",
ai_model: "sonnet",
ai_api_key: "test-anthropic-key",
});

const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(
'export ANTHROPIC_API_KEY=\\"test-anthropic-key\\"',
);
expect(instance.script).toContain("--model sonnet");
expect(instance.script).toContain(
"Starting Aider using anthropic provider and model: sonnet",
);
});

it("handles custom provider with custom env var and API key", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
ai_provider: "custom",
custom_env_var_name: "MY_CUSTOM_API_KEY",
ai_model: "custom-model",
ai_api_key: "test-custom-key",
});

const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(
'export MY_CUSTOM_API_KEY=\\"test-custom-key\\"',
);
expect(instance.script).toContain("--model custom-model");
expect(instance.script).toContain(
"Starting Aider using custom provider and model: custom-model",
);
});
});
179 changes: 179 additions & 0 deletions aider-test-template/aider/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
terraform {
required_version = ">= 1.0"

required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
}
}
}

variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}

data "coder_workspace" "me" {}

data "coder_workspace_owner" "me" {}

variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}

variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}

variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/aider.svg"
}

variable "folder" {
type = string
description = "The folder to run Aider in."
default = "/home/coder"
}

variable "install_aider" {
type = bool
description = "Whether to install Aider."
default = true
}

variable "aider_version" {
type = string
description = "The version of Aider to install."
default = "latest"
}

variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "latest"
}

variable "experiment_pre_install_script" {
type = string
description = "Custom script to run before installing Aider."
default = null
}

variable "experiment_post_install_script" {
type = string
description = "Custom script to run after installing Aider."
default = null
}

variable "experiment_additional_extensions" {
type = string
description = "Additional extensions configuration in YAML format to append to the config."
default = null
}

variable "ai_provider" {
type = string
description = "AI provider to use with Aider (openai, anthropic, azure, google, etc.)"
default = "anthropic"
validation {
condition = contains(["openai", "anthropic", "azure", "google", "cohere", "mistral", "ollama", "custom"], var.ai_provider)
error_message = "ai_provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
}
}

variable "ai_model" {
type = string
description = "AI model to use with Aider. Can use Aider's built-in aliases like '4o' (gpt-4o), 'sonnet' (claude-3-7-sonnet), 'opus' (claude-3-opus), etc."
default = "sonnet"
}

variable "ai_api_key" {
type = string
description = "API key for the selected AI provider. This will be set as the appropriate environment variable based on the provider."
default = ""
sensitive = true
}

variable "custom_env_var_name" {
type = string
description = "Custom environment variable name when using custom provider"
default = ""
}

locals {
app_slug = "aider"
base_extensions = <<-EOT
coder:
args:
- exp
- mcp
- server
cmd: coder
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
enabled: true
envs:
CODER_MCP_APP_STATUS_SLUG: ${local.app_slug}
CODER_MCP_AI_AGENTAPI_URL: http://localhost:3284
name: Coder
timeout: 3000
type: stdio
developer:
display_name: Developer
enabled: true
name: developer
timeout: 300
type: builtin
EOT

# Add two spaces to each line of extensions to match YAML structure
formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : ""
combined_extensions = <<-EOT
extensions:
${local.formatted_base}${local.additional_extensions}
EOT
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".aider-module"
}

module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.0.1"

agent_id = var.agent_id
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = "Aider"
cli_app_slug = "${local.app_slug}-cli"
cli_app_display_name = "Aider CLI"
module_dir_name = local.module_dir_name
agentapi_version = var.agentapi_version
pre_install_script = var.experiment_pre_install_script
post_install_script = var.experiment_post_install_script
start_script = local.start_script
install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail

echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh

ARG_PROVIDER='${var.ai_provider}' \
ARG_MODEL='${var.ai_model}' \
ARG_AIDER_CONFIG="$(echo -n '${base64encode(local.combined_extensions)}' | base64 -d)" \
ARG_INSTALL='${var.install_aider}' \
ARG_AIDER_VERSION='${var.aider_version}' \
/tmp/install.sh
EOT
}
71 changes: 71 additions & 0 deletions aider-test-template/aider/scripts/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/bin/bash

# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}

set -o nounset

echo "--------------------------------"
echo "provider: $ARG_PROVIDER"
echo "model: $ARG_MODEL"
echo "aider_config: $ARG_AIDER_CONFIG"
echo "install: $ARG_INSTALL"
echo "aider_version: $ARG_AIDER_VERSION"
echo "--------------------------------"

set +o nounset

if [ "${ARG_INSTALL}" = "true" ]; then
echo "Installing Aider..."
if ! command_exists python3 || ! command_exists pip3; then
echo "Installing Python dependencies required for Aider..."
if command -v apt-get >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y -qq python3-pip python3-venv
else
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
apt-get install -y -qq python3-pip python3-venv || echo "Warning: Cannot install Python packages without sudo privileges"
fi
elif command -v dnf >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo dnf install -y -q python3-pip python3-virtualenv
else
dnf install -y -q python3-pip python3-virtualenv || echo "Warning: Cannot install Python packages without sudo privileges"
fi
else
echo "Warning: Unable to install Python on this system. Neither apt-get nor dnf found."
fi
else
echo "Python is already installed, skipping installation."
fi

if ! command_exists aider; then
curl -LsSf https://aider.chat/install.sh | sh
fi

if [ -f "$HOME/.bashrc" ]; then
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc"
fi
fi

if [ -f "$HOME/.zshrc" ]; then
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc"
fi
fi
else
echo "Skipping Aider installation"
fi

if [ "${ARG_AIDER_CONFIG}" != "" ]; then
echo "Configuring Aider..."
mkdir -p "$HOME/.config/aider"
echo "model: $ARG_MODEL" > "$HOME/.config/aider/config.yml"
echo "$ARG_AIDER_CONFIG" >> "$HOME/.config/aider/config.yml"
else
echo "Skipping Aider configuration"
fi
35 changes: 35 additions & 0 deletions aider-test-template/aider/scripts/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash

set -o errexit
set -o pipefail

command_exists() {
command -v "$1" >/dev/null 2>&1
}

if command_exists aider; then
AIDER_CMD=aider
elif [ -f "$HOME/.local/bin/aider" ]; then
AIDER_CMD="$HOME/.local/bin/aider"
else
echo "Error: Aider is not installed. Please enable install_aider or install it manually."
exit 1
fi

# this must be kept up to date with main.tf
MODULE_DIR="$HOME/.aider-module"
mkdir -p "$MODULE_DIR"

PROMPT_FILE="$MODULE_DIR/prompt.txt"

if [ -n "${AIDER_TASK_PROMPT}" ]; then
echo "Starting with a prompt"
echo -n "${AIDER_TASK_PROMPT}" >"$PROMPT_FILE"
AIDER_ARGS=(--message-file "$PROMPT_FILE")
else
echo "Starting without a prompt"
AIDER_ARGS=()
fi

agentapi server --term-width 67 --term-height 1190 -- \
bash -c "$(printf '%q ' "$AIDER_CMD" "${AIDER_ARGS[@]}")"
Loading