Skip to content

feat: update the goose module to support Tasks #178

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

Open
wants to merge 1 commit into
base: hugodutka/agentapi-module
Choose a base branch
from
Open
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
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"devDependencies": {
"@types/bun": "^1.2.9",
"bun-types": "^1.1.23",
"dedent": "^1.6.0",
"gray-matter": "^4.0.3",
"marked": "^12.0.2",
"prettier": "^3.3.3",
Expand Down
75 changes: 18 additions & 57 deletions registry/coder/modules/goose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Run Goose in your workspace
icon: ../../../../.icons/goose.svg
maintainer_github: coder
verified: true
tags: [agent, goose, ai]
tags: [agent, goose, ai, tasks]
---

# Goose
Expand All @@ -13,36 +13,26 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener

```tf
module "goose" {
source = "registry.coder.com/coder/goose/coder"
version = "1.3.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
goose_version = "v1.0.16"
source = "registry.coder.com/coder/goose/coder"
version = "2.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
goose_version = "v1.0.31"
goose_provider = "anthropic"
goose_model = "claude-3-5-sonnet-latest"
}
```

## Prerequisites

- `screen` or `tmux` must be installed in your workspace to run Goose in the background
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template

The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.

## Examples

Your workspace must have `screen` or `tmux` installed to use the background session functionality.

### Run in the background and report tasks (Experimental)

> This functionality is in early access as of Coder v2.21 and is still evolving.
> For now, we recommend testing it in a demo or staging environment,
> rather than deploying to production
>
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
>
> Join our [Discord channel](https://discord.gg/coder) or
> [contact us](https://coder.com/contact) to get help or share feedback.
### Run in the background and report tasks

```tf
module "coder-login" {
Expand Down Expand Up @@ -81,7 +71,6 @@ resource "coder_agent" "main" {
EOT
GOOSE_TASK_PROMPT = data.coder_parameter.ai_prompt.value

# An API key is required for experiment_auto_configure
# See https://block.github.io/goose/docs/getting-started/providers
ANTHROPIC_API_KEY = var.anthropic_api_key # or use a coder_parameter
}
Expand All @@ -90,28 +79,14 @@ resource "coder_agent" "main" {
module "goose" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/goose/coder"
version = "1.3.0"
version = "2.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
goose_version = "v1.0.16"

# Enable experimental features
experiment_report_tasks = true

# Run Goose in the background with screen (pick one: screen or tmux)
experiment_use_screen = true
# experiment_use_tmux = true # Alternative: use tmux instead of screen

# Optional: customize the session name (defaults to "goose")
# session_name = "goose-session"

# Avoid configuring Goose manually
experiment_auto_configure = true
goose_version = "v1.0.31"

# Required for experiment_auto_configure
experiment_goose_provider = "anthropic"
experiment_goose_model = "claude-3-5-sonnet-latest"
goose_provider = "anthropic"
goose_model = "claude-3-5-sonnet-latest"
}
```

Expand All @@ -123,11 +98,11 @@ You can extend Goose's capabilities by adding custom extensions. For example, to
module "goose" {
# ... other configuration ...

experiment_pre_install_script = <<-EOT
pre_install_script = <<-EOT
npm i -g @wonderwhy-er/desktop-commander@latest
EOT

experiment_additional_extensions = <<-EOT
additional_extensions = <<-EOT
desktop-commander:
args: []
cmd: desktop-commander
Expand All @@ -145,20 +120,6 @@ This will add the desktop-commander extension to Goose, allowing it to run comma

Note: The indentation in the heredoc is preserved, so you can write the YAML naturally.

## Run standalone
## Troubleshooting

Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or tmux, and without any task reporting to the Coder UI.

```tf
module "goose" {
source = "registry.coder.com/coder/goose/coder"
version = "1.3.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
goose_version = "v1.0.16"

# Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL
icon = "https://raw.githubusercontent.com/block/goose/refs/heads/main/ui/desktop/src/images/icon.svg"
}
```
The module will create log files in the workspace's `~/.goose-module` directory. If you run into any issues, look at them for more information.
254 changes: 254 additions & 0 deletions registry/coder/modules/goose/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import {
test,
afterEach,
describe,
setDefaultTimeout,
beforeAll,
expect,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
loadTestFile,
writeExecutable,
setup as setupUtil,
execModuleScript,
expectAgentAPIStarted,
} from "../agentapi/test-util";
import dedent from "dedent";

let cleanupFunctions: (() => Promise<void>)[] = [];

const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};

// Cleanup logic depends on the fact that bun's built-in test runner
// runs tests sequentially.
// https://bun.sh/docs/test/discovery#execution-order
// Weird things would happen if tried to run tests in parallel.
// One test could clean up resources that another test was still using.
afterEach(async () => {
// reverse the cleanup functions so that they are run in the correct order
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
for (const cleanup of cleanupFnsCopy) {
try {
await cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}
});

interface SetupProps {
skipAgentAPIMock?: boolean;
skipGooseMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}

const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_goose: props?.skipGooseMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
goose_provider: "test-provider",
goose_model: "test-model",
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
if (!props?.skipGooseMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/goose",
content: await loadTestFile(import.meta.dir, "goose-mock.sh"),
});
}
return { id };
};

// increase the default timeout to 60 seconds
setDefaultTimeout(60 * 1000);

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

test("happy-path", async () => {
const { id } = await setup();

await execModuleScript(id);

await expectAgentAPIStarted(id);
});

test("install-version", async () => {
const { id } = await setup({
skipGooseMock: true,
moduleVariables: {
install_goose: "true",
goose_version: "v1.0.24",
},
});

await execModuleScript(id);

const resp = await execContainer(id, [
"bash",
"-c",
`"$HOME/.local/bin/goose" --version`,
]);
if (resp.exitCode !== 0) {
console.log(resp.stdout);
console.log(resp.stderr);
}
expect(resp.exitCode).toBe(0);
expect(resp.stdout).toContain("1.0.24");
});

test("install-stable", async () => {
const { id } = await setup({
skipGooseMock: true,
moduleVariables: {
install_goose: "true",
goose_version: "stable",
},
});

await execModuleScript(id);

const resp = await execContainer(id, [
"bash",
"-c",
`"$HOME/.local/bin/goose" --version`,
]);
if (resp.exitCode !== 0) {
console.log(resp.stdout);
console.log(resp.stderr);
}
expect(resp.exitCode).toBe(0);
});

test("config", async () => {
const expected =
dedent`
GOOSE_PROVIDER: anthropic
GOOSE_MODEL: claude-3-5-sonnet-latest
extensions:
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: goose
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
custom-stuff:
enabled: true
name: custom-stuff
timeout: 300
type: builtin
`.trim() + "\n";

const { id } = await setup({
moduleVariables: {
goose_provider: "anthropic",
goose_model: "claude-3-5-sonnet-latest",
additional_extensions: dedent`
custom-stuff:
enabled: true
name: custom-stuff
timeout: 300
type: builtin
`.trim(),
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.config/goose/config.yaml",
);
expect(resp).toEqual(expected);
});

test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
post_install_script: "#!/bin/bash\necho 'post-install-script'",
},
});

await execModuleScript(id);

const preInstallLog = await readFileContainer(
id,
"/home/coder/.goose-module/pre_install.log",
);
expect(preInstallLog).toContain("pre-install-script");

const postInstallLog = await readFileContainer(
id,
"/home/coder/.goose-module/post_install.log",
);
expect(postInstallLog).toContain("post-install-script");
});

const promptFile = "/home/coder/.goose-module/prompt.txt";
const agentapiStartLog = "/home/coder/.goose-module/agentapi-start.log";

test("start-with-prompt", async () => {
const { id } = await setup({
agentapiMockScript: await loadTestFile(
import.meta.dir,
"agentapi-mock-print-args.js",
),
});
await execModuleScript(id, {
GOOSE_TASK_PROMPT: "custom-test-prompt",
});
const prompt = await readFileContainer(id, promptFile);
expect(prompt).toContain("custom-test-prompt");

const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
expect(agentapiMockOutput).toContain(
"'goose run --interactive --instructions /home/coder/.goose-module/prompt.txt '",
);
});

test("start-without-prompt", async () => {
const { id } = await setup({
agentapiMockScript: await loadTestFile(
import.meta.dir,
"agentapi-mock-print-args.js",
),
});
await execModuleScript(id);

const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
expect(agentapiMockOutput).toContain("'goose '");

const prompt = await execContainer(id, ["ls", "-l", promptFile]);
expect(prompt.exitCode).not.toBe(0);
expect(prompt.stderr).toContain("No such file or directory");
});
});
Loading