diff --git a/registry/coder/modules/goose/README.md b/registry/coder/modules/goose/README.md index f4f91ab5..5081564e 100644 --- a/registry/coder/modules/goose/README.md +++ b/registry/coder/modules/goose/README.md @@ -30,6 +30,10 @@ module "goose" { The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces. +### Session Resumption Behavior + +By default, Goose automatically resumes workspace-specific sessions when your workspace restarts. Sessions are named `task-{workspace_name}`, ensuring each workspace maintains its own conversation history. If no session exists (first start), your task prompt will run normally. To disable this and always start fresh, set `continue = false`. + ## Examples ### Run in the background and report tasks diff --git a/registry/coder/modules/goose/main.test.ts b/registry/coder/modules/goose/main.test.ts index ae5635b0..407ab610 100644 --- a/registry/coder/modules/goose/main.test.ts +++ b/registry/coder/modules/goose/main.test.ts @@ -291,4 +291,92 @@ describe("goose", async () => { expect(agentapiMockOutput).toMatch(/AGENTAPI_CHAT_BASE_PATH=$/m); }); }); + + describe("session management", async () => { + test("session-name-new", async () => { + const { id } = await setup({ + agentapiMockScript: await loadTestFile( + import.meta.dir, + "agentapi-mock-print-args.js", + ), + moduleVariables: { + session_name: "my-custom-session", + }, + }); + + await execModuleScript(id); + + const agentapiMockOutput = await readFileContainer(id, agentapiStartLog); + expect(agentapiMockOutput).toContain("Session name: my-custom-session"); + expect(agentapiMockOutput).toContain("Starting new named session: my-custom-session"); + }); + + test("session-name-resume", async () => { + const { id } = await setup({ + agentapiMockScript: await loadTestFile( + import.meta.dir, + "agentapi-mock-print-args.js", + ), + moduleVariables: { + session_name: "existing-session", + }, + }); + + await execContainer(id, [ + "bash", + "-c", + dedent` + cat > /usr/bin/goose <<'EOF' +#!/bin/bash +if [ "$1" = "session" ] && [ "$2" = "list" ] && [ "$3" = "--format" ] && [ "$4" = "json" ]; then + echo '[{"id":"test123","name":"existing-session"}]' +else + echo "goose mock" +fi +EOF + chmod +x /usr/bin/goose + `, + ]); + + await execModuleScript(id); + + const agentapiMockOutput = await readFileContainer(id, agentapiStartLog); + expect(agentapiMockOutput).toContain("Resuming session by name: existing-session"); + }); + + test("default-session-name", async () => { + const { id } = await setup({ + agentapiMockScript: await loadTestFile( + import.meta.dir, + "agentapi-mock-print-args.js", + ), + moduleVariables: { + continue: "true", + }, + }); + + await execModuleScript(id); + + const agentapiMockOutput = await readFileContainer(id, agentapiStartLog); + expect(agentapiMockOutput).toContain("Session name: task-"); + expect(agentapiMockOutput).toContain("Starting new session:"); + }); + + test("continue-disabled", async () => { + const { id } = await setup({ + agentapiMockScript: await loadTestFile( + import.meta.dir, + "agentapi-mock-print-args.js", + ), + moduleVariables: { + continue: "false", + }, + }); + + await execModuleScript(id); + + const agentapiMockOutput = await readFileContainer(id, agentapiStartLog); + expect(agentapiMockOutput).toContain("Continue disabled, starting fresh session"); + }); + }); }); diff --git a/registry/coder/modules/goose/main.tf b/registry/coder/modules/goose/main.tf index 51f8b6d6..9721e062 100644 --- a/registry/coder/modules/goose/main.tf +++ b/registry/coder/modules/goose/main.tf @@ -100,9 +100,22 @@ variable "additional_extensions" { default = null } +variable "session_name" { + type = string + description = "Name for the Goose session. If empty, uses 'task-{workspace_name}' for automatic workspace-specific session naming." + default = "" +} + +variable "continue" { + type = bool + description = "Automatically continue existing sessions on workspace restart. When true, resumes session by name if it exists, otherwise starts new named session. When false, always starts fresh." + default = true +} + locals { - app_slug = "goose" - base_extensions = <<-EOT + app_slug = "goose" + default_session_name = "task-${data.coder_workspace.me.name}" + base_extensions = <<-EOT coder: args: - exp @@ -156,8 +169,20 @@ module "agentapi" { agentapi_subdomain = var.subdomain pre_install_script = var.pre_install_script post_install_script = var.post_install_script - start_script = local.start_script folder = local.folder + start_script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh + chmod +x /tmp/start.sh + + ARG_SESSION_NAME='${var.session_name}' \ + ARG_DEFAULT_SESSION_NAME='${local.default_session_name}' \ + ARG_CONTINUE='${var.continue}' \ + /tmp/start.sh + EOT install_script = <<-EOT #!/bin/bash set -o errexit diff --git a/registry/coder/modules/goose/main.tftest.hcl b/registry/coder/modules/goose/main.tftest.hcl new file mode 100644 index 00000000..0cfc0506 --- /dev/null +++ b/registry/coder/modules/goose/main.tftest.hcl @@ -0,0 +1,105 @@ +run "test_goose_basic" { + command = plan + + variables { + agent_id = "test-agent-123" + goose_provider = "anthropic" + goose_model = "claude-3-5-sonnet-latest" + } + + assert { + condition = var.goose_provider == "anthropic" + error_message = "Goose provider variable should be set correctly" + } + + assert { + condition = var.goose_model == "claude-3-5-sonnet-latest" + error_message = "Goose model variable should be set correctly" + } + + assert { + condition = var.install_goose == true + error_message = "Install goose should default to true" + } + + assert { + condition = var.install_agentapi == true + error_message = "Install agentapi should default to true" + } + + assert { + condition = var.continue == true + error_message = "Continue should default to true" + } + +} + +run "test_goose_with_session_name" { + command = plan + + variables { + agent_id = "test-agent-456" + goose_provider = "anthropic" + goose_model = "claude-3-5-sonnet-latest" + session_name = "my-custom-session" + } + + assert { + condition = var.session_name == "my-custom-session" + error_message = "Session name should be set to my-custom-session" + } +} + +run "test_goose_continue_disabled" { + command = plan + + variables { + agent_id = "test-agent-789" + goose_provider = "anthropic" + goose_model = "claude-3-5-sonnet-latest" + continue = false + } + + assert { + condition = var.continue == false + error_message = "Continue should be set to false" + } +} + +run "test_goose_default_session_name" { + command = plan + + variables { + agent_id = "test-agent-101" + goose_provider = "anthropic" + goose_model = "claude-3-5-sonnet-latest" + } + + assert { + condition = length(regexall("task-", local.default_session_name)) > 0 + error_message = "Default session name should contain task- prefix" + } +} + +run "test_goose_with_additional_extensions" { + command = plan + + variables { + agent_id = "test-agent-202" + goose_provider = "anthropic" + goose_model = "claude-3-5-sonnet-latest" + additional_extensions = <<-EOT +custom-extension: + enabled: true + name: custom + timeout: 300 + type: builtin +EOT + } + + assert { + condition = var.additional_extensions != null + error_message = "Additional extensions should be set" + } +} + diff --git a/registry/coder/modules/goose/scripts/start.sh b/registry/coder/modules/goose/scripts/start.sh index 737138ba..0ad2246c 100644 --- a/registry/coder/modules/goose/scripts/start.sh +++ b/registry/coder/modules/goose/scripts/start.sh @@ -16,19 +16,51 @@ else exit 1 fi -# this must be kept up to date with main.tf MODULE_DIR="$HOME/.goose-module" mkdir -p "$MODULE_DIR" -if [ ! -z "$GOOSE_TASK_PROMPT" ]; then - echo "Starting with a prompt" - PROMPT="Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT" - PROMPT_FILE="$MODULE_DIR/prompt.txt" - echo -n "$PROMPT" > "$PROMPT_FILE" - GOOSE_ARGS=(run --interactive --instructions "$PROMPT_FILE") +ARG_SESSION_NAME=${ARG_SESSION_NAME:-} +ARG_DEFAULT_SESSION_NAME=${ARG_DEFAULT_SESSION_NAME:-} +ARG_CONTINUE=${ARG_CONTINUE:-true} + +if [ -n "$ARG_SESSION_NAME" ]; then + SESSION_NAME="$ARG_SESSION_NAME" else - echo "Starting without a prompt" - GOOSE_ARGS=() + SESSION_NAME="$ARG_DEFAULT_SESSION_NAME" +fi + +echo "Session name: $SESSION_NAME" + +session_name_exists() { + local name=$1 + "$GOOSE_CMD" session list --format json 2>/dev/null | grep -q "\"name\":[[:space:]]*\"$name\"" +} + +if [ "$ARG_CONTINUE" = "true" ]; then + if session_name_exists "$SESSION_NAME"; then + echo "Resuming session: $SESSION_NAME" + GOOSE_ARGS=(session --resume --name "$SESSION_NAME") + else + echo "Starting new session: $SESSION_NAME" + if [ -n "$GOOSE_TASK_PROMPT" ]; then + PROMPT="Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT" + PROMPT_FILE="$MODULE_DIR/prompt.txt" + echo -n "$PROMPT" > "$PROMPT_FILE" + GOOSE_ARGS=(run --interactive --name "$SESSION_NAME" --instructions "$PROMPT_FILE") + else + GOOSE_ARGS=(session --name "$SESSION_NAME") + fi + fi +else + echo "Continue disabled, starting fresh session" + if [ -n "$GOOSE_TASK_PROMPT" ]; then + PROMPT="Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT" + PROMPT_FILE="$MODULE_DIR/prompt.txt" + echo -n "$PROMPT" > "$PROMPT_FILE" + GOOSE_ARGS=(run --interactive --instructions "$PROMPT_FILE") + else + GOOSE_ARGS=(session) + fi fi agentapi server --term-width 67 --term-height 1190 -- \