Skip to content

Commit 2169fb0

Browse files
authored
feat(coder/modules/agentapi): add state persistence support (#736)
AgentAPI can now save and restore conversation state across workspace restarts. The module exports env vars (AGENTAPI_STATE_FILE, AGENTAPI_SAVE_STATE, AGENTAPI_LOAD_STATE, AGENTAPI_PID_FILE) that the binary reads directly. No consumer module changes needed. New variables: enable_state_persistence (default false), state_file_path, pid_file_path. State and PID files default to $HOME/<module_dir_name>/. Requires agentapi >= v0.12.0. A shared version_at_least function in lib.sh gates the env var exports and SIGUSR1 in the shutdown script. Old binaries get a warning and graceful skip. Shutdown script now does SIGUSR1 (state save), log snapshot capture (existing, now fault-tolerant via subshell), then SIGTERM with wait. Closes coder/internal#1257 Refs coder/internal#1256 Refs #696
1 parent e3abbb9 commit 2169fb0

9 files changed

Lines changed: 542 additions & 18 deletions

File tree

registry/coder/modules/agentapi/README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
1616
```tf
1717
module "agentapi" {
1818
source = "registry.coder.com/coder/agentapi/coder"
19-
version = "2.1.1"
19+
version = "2.2.0"
2020
2121
agent_id = var.agent_id
2222
web_app_slug = local.app_slug
@@ -62,6 +62,33 @@ module "agentapi" {
6262
}
6363
```
6464

65+
## State Persistence
66+
67+
AgentAPI can save and restore conversation state across workspace restarts.
68+
This is disabled by default and requires agentapi binary >= v0.12.0.
69+
70+
State and PID files are stored in `$HOME/<module_dir_name>/` alongside other
71+
module files (e.g. `$HOME/.claude-module/agentapi-state.json`).
72+
73+
To enable:
74+
75+
```tf
76+
module "agentapi" {
77+
# ... other config
78+
enable_state_persistence = true
79+
}
80+
```
81+
82+
To override file paths:
83+
84+
```tf
85+
module "agentapi" {
86+
# ... other config
87+
state_file_path = "/custom/path/state.json"
88+
pid_file_path = "/custom/path/agentapi.pid"
89+
}
90+
```
91+
6592
## For module developers
6693

6794
For a complete example of how to use this module, see the [Goose module](https://github.com/coder/registry/blob/main/registry/coder/modules/goose/main.tf).
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
mock_provider "coder" {}
2+
3+
variables {
4+
agent_id = "test-agent"
5+
web_app_icon = "/icon/test.svg"
6+
web_app_display_name = "Test"
7+
web_app_slug = "test"
8+
cli_app_display_name = "Test CLI"
9+
cli_app_slug = "test-cli"
10+
start_script = "echo test"
11+
module_dir_name = ".test-module"
12+
}
13+
14+
run "default_values" {
15+
command = plan
16+
17+
assert {
18+
condition = var.enable_state_persistence == false
19+
error_message = "enable_state_persistence should default to false"
20+
}
21+
22+
assert {
23+
condition = var.state_file_path == ""
24+
error_message = "state_file_path should default to empty string"
25+
}
26+
27+
assert {
28+
condition = var.pid_file_path == ""
29+
error_message = "pid_file_path should default to empty string"
30+
}
31+
32+
# Verify start script contains state persistence ARG_ vars.
33+
assert {
34+
condition = can(regex("ARG_ENABLE_STATE_PERSISTENCE", coder_script.agentapi.script))
35+
error_message = "start script should contain ARG_ENABLE_STATE_PERSISTENCE"
36+
}
37+
38+
assert {
39+
condition = can(regex("ARG_STATE_FILE_PATH", coder_script.agentapi.script))
40+
error_message = "start script should contain ARG_STATE_FILE_PATH"
41+
}
42+
43+
assert {
44+
condition = can(regex("ARG_PID_FILE_PATH", coder_script.agentapi.script))
45+
error_message = "start script should contain ARG_PID_FILE_PATH"
46+
}
47+
48+
# Verify shutdown script contains PID-related ARG_ vars.
49+
assert {
50+
condition = can(regex("ARG_PID_FILE_PATH", coder_script.agentapi_shutdown.script))
51+
error_message = "shutdown script should contain ARG_PID_FILE_PATH"
52+
}
53+
54+
assert {
55+
condition = can(regex("ARG_MODULE_DIR_NAME", coder_script.agentapi_shutdown.script))
56+
error_message = "shutdown script should contain ARG_MODULE_DIR_NAME"
57+
}
58+
59+
assert {
60+
condition = can(regex("ARG_ENABLE_STATE_PERSISTENCE", coder_script.agentapi_shutdown.script))
61+
error_message = "shutdown script should contain ARG_ENABLE_STATE_PERSISTENCE"
62+
}
63+
}
64+
65+
run "state_persistence_disabled" {
66+
command = plan
67+
68+
variables {
69+
enable_state_persistence = false
70+
}
71+
72+
assert {
73+
condition = var.enable_state_persistence == false
74+
error_message = "enable_state_persistence should be false"
75+
}
76+
77+
# Even when disabled, the ARG_ vars should still be in the script
78+
# (the shell script handles the conditional logic).
79+
assert {
80+
condition = can(regex("ARG_ENABLE_STATE_PERSISTENCE='false'", coder_script.agentapi.script))
81+
error_message = "start script should contain ARG_ENABLE_STATE_PERSISTENCE='false'"
82+
}
83+
}
84+
85+
run "custom_paths" {
86+
command = plan
87+
88+
variables {
89+
state_file_path = "/custom/state.json"
90+
pid_file_path = "/custom/agentapi.pid"
91+
}
92+
93+
assert {
94+
condition = can(regex("/custom/state.json", coder_script.agentapi.script))
95+
error_message = "start script should contain custom state_file_path"
96+
}
97+
98+
assert {
99+
condition = can(regex("/custom/agentapi.pid", coder_script.agentapi.script))
100+
error_message = "start script should contain custom pid_file_path"
101+
}
102+
103+
# Verify custom paths also appear in shutdown script.
104+
assert {
105+
condition = can(regex("/custom/agentapi.pid", coder_script.agentapi_shutdown.script))
106+
error_message = "shutdown script should contain custom pid_file_path"
107+
}
108+
}

0 commit comments

Comments
 (0)