Skip to content

Commit 535eaea

Browse files
committed
feat(aider): Add Coder Tasks and AgentAPI support
1 parent f04d7d2 commit 535eaea

File tree

8 files changed

+613
-0
lines changed

8 files changed

+613
-0
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describe, expect, it } from "bun:test";
2+
import {
3+
findResourceInstance,
4+
runTerraformApply,
5+
runTerraformInit,
6+
testRequiredVariables,
7+
} from "~test";
8+
9+
describe("aider", async () => {
10+
await runTerraformInit(import.meta.dir);
11+
12+
testRequiredVariables(import.meta.dir, {
13+
agent_id: "foo",
14+
});
15+
16+
it("configures task prompt correctly", async () => {
17+
const testPrompt = "Add a hello world function";
18+
const state = await runTerraformApply(import.meta.dir, {
19+
agent_id: "foo",
20+
task_prompt: testPrompt,
21+
});
22+
23+
const instance = findResourceInstance(state, "coder_script");
24+
expect(instance.script).toContain(
25+
`This is your current task: ${testPrompt}`,
26+
);
27+
expect(instance.script).toContain("aider --architect --yes-always");
28+
});
29+
30+
it("handles custom system prompt", async () => {
31+
const customPrompt = "Report all tasks with state: working";
32+
const state = await runTerraformApply(import.meta.dir, {
33+
agent_id: "foo",
34+
system_prompt: customPrompt,
35+
});
36+
37+
const instance = findResourceInstance(state, "coder_script");
38+
expect(instance.script).toContain(customPrompt);
39+
});
40+
41+
it("handles pre and post install scripts", async () => {
42+
const state = await runTerraformApply(import.meta.dir, {
43+
agent_id: "foo",
44+
experiment_pre_install_script: "echo 'Pre-install script executed'",
45+
experiment_post_install_script: "echo 'Post-install script executed'",
46+
});
47+
48+
const instance = findResourceInstance(state, "coder_script");
49+
50+
expect(instance.script).toContain("Running pre-install script");
51+
expect(instance.script).toContain("Running post-install script");
52+
expect(instance.script).toContain("base64 -d > /tmp/pre_install.sh");
53+
expect(instance.script).toContain("base64 -d > /tmp/post_install.sh");
54+
});
55+
56+
it("validates that use_screen and use_tmux cannot both be true", async () => {
57+
const state = await runTerraformApply(import.meta.dir, {
58+
agent_id: "foo",
59+
use_screen: true,
60+
use_tmux: true,
61+
});
62+
63+
const instance = findResourceInstance(state, "coder_script");
64+
65+
expect(instance.script).toContain(
66+
"Error: Both use_screen and use_tmux cannot be enabled at the same time",
67+
);
68+
expect(instance.script).toContain("exit 1");
69+
});
70+
71+
it("configures Aider with known provider and model", async () => {
72+
const state = await runTerraformApply(import.meta.dir, {
73+
agent_id: "foo",
74+
ai_provider: "anthropic",
75+
ai_model: "sonnet",
76+
ai_api_key: "test-anthropic-key",
77+
});
78+
79+
const instance = findResourceInstance(state, "coder_script");
80+
expect(instance.script).toContain(
81+
'export ANTHROPIC_API_KEY=\\"test-anthropic-key\\"',
82+
);
83+
expect(instance.script).toContain("--model sonnet");
84+
expect(instance.script).toContain(
85+
"Starting Aider using anthropic provider and model: sonnet",
86+
);
87+
});
88+
89+
it("handles custom provider with custom env var and API key", async () => {
90+
const state = await runTerraformApply(import.meta.dir, {
91+
agent_id: "foo",
92+
ai_provider: "custom",
93+
custom_env_var_name: "MY_CUSTOM_API_KEY",
94+
ai_model: "custom-model",
95+
ai_api_key: "test-custom-key",
96+
});
97+
98+
const instance = findResourceInstance(state, "coder_script");
99+
expect(instance.script).toContain(
100+
'export MY_CUSTOM_API_KEY=\\"test-custom-key\\"',
101+
);
102+
expect(instance.script).toContain("--model custom-model");
103+
expect(instance.script).toContain(
104+
"Starting Aider using custom provider and model: custom-model",
105+
);
106+
});
107+
});

aider-test-template/aider/main.tf

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
terraform {
2+
required_version = ">= 1.0"
3+
4+
required_providers {
5+
coder = {
6+
source = "coder/coder"
7+
version = ">= 2.7"
8+
}
9+
}
10+
}
11+
12+
variable "agent_id" {
13+
type = string
14+
description = "The ID of a Coder agent."
15+
}
16+
17+
data "coder_workspace" "me" {}
18+
19+
data "coder_workspace_owner" "me" {}
20+
21+
variable "order" {
22+
type = number
23+
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)."
24+
default = null
25+
}
26+
27+
variable "group" {
28+
type = string
29+
description = "The name of a group that this app belongs to."
30+
default = null
31+
}
32+
33+
variable "icon" {
34+
type = string
35+
description = "The icon to use for the app."
36+
default = "/icon/aider.svg"
37+
}
38+
39+
variable "folder" {
40+
type = string
41+
description = "The folder to run Aider in."
42+
default = "/home/coder"
43+
}
44+
45+
variable "install_aider" {
46+
type = bool
47+
description = "Whether to install Aider."
48+
default = true
49+
}
50+
51+
variable "aider_version" {
52+
type = string
53+
description = "The version of Aider to install."
54+
default = "latest"
55+
}
56+
57+
variable "agentapi_version" {
58+
type = string
59+
description = "The version of AgentAPI to install."
60+
default = "latest"
61+
}
62+
63+
variable "experiment_pre_install_script" {
64+
type = string
65+
description = "Custom script to run before installing Aider."
66+
default = null
67+
}
68+
69+
variable "experiment_post_install_script" {
70+
type = string
71+
description = "Custom script to run after installing Aider."
72+
default = null
73+
}
74+
75+
variable "experiment_additional_extensions" {
76+
type = string
77+
description = "Additional extensions configuration in YAML format to append to the config."
78+
default = null
79+
}
80+
81+
variable "ai_provider" {
82+
type = string
83+
description = "AI provider to use with Aider (openai, anthropic, azure, google, etc.)"
84+
default = "anthropic"
85+
validation {
86+
condition = contains(["openai", "anthropic", "azure", "google", "cohere", "mistral", "ollama", "custom"], var.ai_provider)
87+
error_message = "ai_provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
88+
}
89+
}
90+
91+
variable "ai_model" {
92+
type = string
93+
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."
94+
default = "sonnet"
95+
}
96+
97+
variable "ai_api_key" {
98+
type = string
99+
description = "API key for the selected AI provider. This will be set as the appropriate environment variable based on the provider."
100+
default = ""
101+
sensitive = true
102+
}
103+
104+
variable "custom_env_var_name" {
105+
type = string
106+
description = "Custom environment variable name when using custom provider"
107+
default = ""
108+
}
109+
110+
locals {
111+
app_slug = "aider"
112+
base_extensions = <<-EOT
113+
coder:
114+
args:
115+
- exp
116+
- mcp
117+
- server
118+
cmd: coder
119+
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
120+
enabled: true
121+
envs:
122+
CODER_MCP_APP_STATUS_SLUG: ${local.app_slug}
123+
CODER_MCP_AI_AGENTAPI_URL: http://localhost:3284
124+
name: Coder
125+
timeout: 3000
126+
type: stdio
127+
developer:
128+
display_name: Developer
129+
enabled: true
130+
name: developer
131+
timeout: 300
132+
type: builtin
133+
EOT
134+
135+
# Add two spaces to each line of extensions to match YAML structure
136+
formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
137+
additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : ""
138+
combined_extensions = <<-EOT
139+
extensions:
140+
${local.formatted_base}${local.additional_extensions}
141+
EOT
142+
install_script = file("${path.module}/scripts/install.sh")
143+
start_script = file("${path.module}/scripts/start.sh")
144+
module_dir_name = ".aider-module"
145+
}
146+
147+
module "agentapi" {
148+
source = "registry.coder.com/coder/agentapi/coder"
149+
version = "1.0.1"
150+
151+
agent_id = var.agent_id
152+
web_app_slug = local.app_slug
153+
web_app_order = var.order
154+
web_app_group = var.group
155+
web_app_icon = var.icon
156+
web_app_display_name = "Aider"
157+
cli_app_slug = "${local.app_slug}-cli"
158+
cli_app_display_name = "Aider CLI"
159+
module_dir_name = local.module_dir_name
160+
agentapi_version = var.agentapi_version
161+
pre_install_script = var.experiment_pre_install_script
162+
post_install_script = var.experiment_post_install_script
163+
start_script = local.start_script
164+
install_script = <<-EOT
165+
#!/bin/bash
166+
set -o errexit
167+
set -o pipefail
168+
169+
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
170+
chmod +x /tmp/install.sh
171+
172+
ARG_PROVIDER='${var.ai_provider}' \
173+
ARG_MODEL='${var.ai_model}' \
174+
ARG_AIDER_CONFIG="$(echo -n '${base64encode(local.combined_extensions)}' | base64 -d)" \
175+
ARG_INSTALL='${var.install_aider}' \
176+
ARG_AIDER_VERSION='${var.aider_version}' \
177+
/tmp/install.sh
178+
EOT
179+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/bin/bash
2+
3+
# Function to check if a command exists
4+
command_exists() {
5+
command -v "$1" >/dev/null 2>&1
6+
}
7+
8+
set -o nounset
9+
10+
echo "--------------------------------"
11+
echo "provider: $ARG_PROVIDER"
12+
echo "model: $ARG_MODEL"
13+
echo "aider_config: $ARG_AIDER_CONFIG"
14+
echo "install: $ARG_INSTALL"
15+
echo "aider_version: $ARG_AIDER_VERSION"
16+
echo "--------------------------------"
17+
18+
set +o nounset
19+
20+
if [ "${ARG_INSTALL}" = "true" ]; then
21+
echo "Installing Aider..."
22+
if ! command_exists python3 || ! command_exists pip3; then
23+
echo "Installing Python dependencies required for Aider..."
24+
if command -v apt-get >/dev/null 2>&1; then
25+
if command -v sudo >/dev/null 2>&1; then
26+
sudo apt-get update -qq
27+
sudo apt-get install -y -qq python3-pip python3-venv
28+
else
29+
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
30+
apt-get install -y -qq python3-pip python3-venv || echo "Warning: Cannot install Python packages without sudo privileges"
31+
fi
32+
elif command -v dnf >/dev/null 2>&1; then
33+
if command -v sudo >/dev/null 2>&1; then
34+
sudo dnf install -y -q python3-pip python3-virtualenv
35+
else
36+
dnf install -y -q python3-pip python3-virtualenv || echo "Warning: Cannot install Python packages without sudo privileges"
37+
fi
38+
else
39+
echo "Warning: Unable to install Python on this system. Neither apt-get nor dnf found."
40+
fi
41+
else
42+
echo "Python is already installed, skipping installation."
43+
fi
44+
45+
if ! command_exists aider; then
46+
curl -LsSf https://aider.chat/install.sh | sh
47+
fi
48+
49+
if [ -f "$HOME/.bashrc" ]; then
50+
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then
51+
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc"
52+
fi
53+
fi
54+
55+
if [ -f "$HOME/.zshrc" ]; then
56+
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then
57+
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc"
58+
fi
59+
fi
60+
else
61+
echo "Skipping Aider installation"
62+
fi
63+
64+
if [ "${ARG_AIDER_CONFIG}" != "" ]; then
65+
echo "Configuring Aider..."
66+
mkdir -p "$HOME/.config/aider"
67+
echo "model: $ARG_MODEL" > "$HOME/.config/aider/config.yml"
68+
echo "$ARG_AIDER_CONFIG" >> "$HOME/.config/aider/config.yml"
69+
else
70+
echo "Skipping Aider configuration"
71+
fi
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/bash
2+
3+
set -o errexit
4+
set -o pipefail
5+
6+
command_exists() {
7+
command -v "$1" >/dev/null 2>&1
8+
}
9+
10+
if command_exists aider; then
11+
AIDER_CMD=aider
12+
elif [ -f "$HOME/.local/bin/aider" ]; then
13+
AIDER_CMD="$HOME/.local/bin/aider"
14+
else
15+
echo "Error: Aider is not installed. Please enable install_aider or install it manually."
16+
exit 1
17+
fi
18+
19+
# this must be kept up to date with main.tf
20+
MODULE_DIR="$HOME/.aider-module"
21+
mkdir -p "$MODULE_DIR"
22+
23+
PROMPT_FILE="$MODULE_DIR/prompt.txt"
24+
25+
if [ -n "${AIDER_TASK_PROMPT}" ]; then
26+
echo "Starting with a prompt"
27+
echo -n "${AIDER_TASK_PROMPT}" >"$PROMPT_FILE"
28+
AIDER_ARGS=(--message-file "$PROMPT_FILE")
29+
else
30+
echo "Starting without a prompt"
31+
AIDER_ARGS=()
32+
fi
33+
34+
agentapi server --term-width 67 --term-height 1190 -- \
35+
bash -c "$(printf '%q ' "$AIDER_CMD" "${AIDER_ARGS[@]}")"

0 commit comments

Comments
 (0)