Skip to content
This repository was archived by the owner on Sep 11, 2025. It is now read-only.

Commit f79a595

Browse files
feat: list agents on health endpoint (#865)
1 parent 9566a5c commit f79a595

File tree

3 files changed

+91
-41
lines changed

3 files changed

+91
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- chore: rename agent lifecycle methods and APIs [#858](https://github.com/hypermodeinc/modus/pull/858)
99
- feat: enforce WASI reactor mode [#859](https://github.com/hypermodeinc/modus/pull/859)
1010
- feat: return user and chat errors in API response [#863](https://github.com/hypermodeinc/modus/pull/863)
11+
- feat: list agents on health endpoint [#865](https://github.com/hypermodeinc/modus/pull/865)
1112

1213
## 2025-05-22 - Go SDK 0.18.0-alpha.3
1314

runtime/actors/agents.go

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,25 @@ import (
2727
goakt "github.com/tochemey/goakt/v3/actor"
2828
)
2929

30-
type agentInfo struct {
31-
id string
32-
name string
33-
status agentStatus
30+
type AgentInfo struct {
31+
Id string `json:"id"`
32+
Name string `json:"name"`
33+
Status AgentStatus `json:"status"`
3434
}
3535

36-
type agentStatus = string
36+
type AgentStatus = string
3737

3838
const (
39-
agentStatusStarting agentStatus = "starting"
40-
agentStatusRunning agentStatus = "running"
41-
agentStatusSuspending agentStatus = "suspending"
42-
agentStatusSuspended agentStatus = "suspended"
43-
agentStatusResuming agentStatus = "resuming"
44-
agentStatusStopping agentStatus = "stopping"
45-
agentStatusTerminated agentStatus = "terminated"
39+
AgentStatusStarting AgentStatus = "starting"
40+
AgentStatusRunning AgentStatus = "running"
41+
AgentStatusSuspending AgentStatus = "suspending"
42+
AgentStatusSuspended AgentStatus = "suspended"
43+
AgentStatusResuming AgentStatus = "resuming"
44+
AgentStatusStopping AgentStatus = "stopping"
45+
AgentStatusTerminated AgentStatus = "terminated"
4646
)
4747

48-
func StartAgent(ctx context.Context, agentName string) (*agentInfo, error) {
48+
func StartAgent(ctx context.Context, agentName string) (*AgentInfo, error) {
4949
plugin, ok := plugins.GetPluginFromContext(ctx)
5050
if !ok {
5151
return nil, fmt.Errorf("no plugin found in context")
@@ -55,10 +55,10 @@ func StartAgent(ctx context.Context, agentName string) (*agentInfo, error) {
5555
host := wasmhost.GetWasmHost(ctx)
5656
spawnActorForAgent(host, plugin, agentId, agentName, false, nil)
5757

58-
info := &agentInfo{
59-
id: agentId,
60-
name: agentName,
61-
status: agentStatusStarting,
58+
info := &AgentInfo{
59+
Id: agentId,
60+
Name: agentName,
61+
Status: AgentStatusStarting,
6262
}
6363

6464
return info, nil
@@ -81,10 +81,10 @@ func spawnActorForAgent(host wasmhost.WasmHost, plugin *plugins.Plugin, agentId,
8181
actorName := getActorName(agentId)
8282

8383
if resuming {
84-
actor.status = agentStatusResuming
84+
actor.status = AgentStatusResuming
8585
actor.initialState = initialState
8686
} else {
87-
actor.status = agentStatusStarting
87+
actor.status = AgentStatusStarting
8888
}
8989

9090
if _, err := _actorSystem.Spawn(ctx, actorName, actor); err != nil {
@@ -101,7 +101,7 @@ func StopAgent(ctx context.Context, agentId string) bool {
101101
}
102102

103103
actor := pid.Actor().(*wasmAgentActor)
104-
actor.status = agentStatusStopping
104+
actor.status = AgentStatusStopping
105105

106106
if err := pid.Shutdown(ctx); err != nil {
107107
logger.Err(ctx, err).Msg("Error stopping agent.")
@@ -167,10 +167,31 @@ func getActorPid(ctx context.Context, agentId string) (*goakt.PID, error) {
167167
return pid, nil
168168
}
169169

170+
func ListAgents() []AgentInfo {
171+
if _actorSystem == nil {
172+
return nil
173+
}
174+
175+
actors := _actorSystem.Actors()
176+
results := make([]AgentInfo, 0, len(actors))
177+
178+
for _, pid := range actors {
179+
if actor, ok := pid.Actor().(*wasmAgentActor); ok {
180+
results = append(results, AgentInfo{
181+
Id: actor.agentId,
182+
Name: actor.agentName,
183+
Status: actor.status,
184+
})
185+
}
186+
}
187+
188+
return results
189+
}
190+
170191
type wasmAgentActor struct {
171192
agentId string
172193
agentName string
173-
status agentStatus
194+
status AgentStatus
174195
plugin *plugins.Plugin
175196
host wasmhost.WasmHost
176197
module wasm.Module
@@ -191,10 +212,10 @@ func (a *wasmAgentActor) PreStart(ac *goakt.Context) error {
191212
ctx := a.newContext()
192213

193214
switch a.status {
194-
case agentStatusStarting:
215+
case AgentStatusStarting:
195216
logger.Info(ctx).Msg("Starting agent.")
196-
case agentStatusResuming, agentStatusSuspended:
197-
a.status = agentStatusResuming
217+
case AgentStatusResuming, AgentStatusSuspended:
218+
a.status = AgentStatusResuming
198219
logger.Info(ctx).Msg("Resuming agent.")
199220
default:
200221
return fmt.Errorf("invalid agent status for actor PreStart: %s", a.status)
@@ -218,21 +239,21 @@ func (a *wasmAgentActor) PreStart(ac *goakt.Context) error {
218239
return err
219240
}
220241

221-
if a.status == agentStatusResuming {
242+
if a.status == AgentStatusResuming {
222243
if err := a.setAgentState(ctx, a.initialState); err != nil {
223244
logger.Err(ctx, err).Msg("Error resuming agent state.")
224245
}
225246
a.initialState = nil
226247
}
227248

228249
duration := time.Since(start)
229-
if a.status == agentStatusResuming {
250+
if a.status == AgentStatusResuming {
230251
logger.Info(ctx).Msg("Agent resumed successfully.")
231252
} else {
232253
logger.Info(ctx).Dur("duration_ms", duration).Msg("Agent started successfully.")
233254
}
234255

235-
a.status = agentStatusRunning
256+
a.status = AgentStatusRunning
236257

237258
if err := a.saveState(ctx); err != nil {
238259
logger.Err(ctx, err).Msg("Error saving agent state.")
@@ -246,10 +267,10 @@ func (a *wasmAgentActor) PostStop(ac *goakt.Context) error {
246267
defer a.module.Close(ctx)
247268

248269
switch a.status {
249-
case agentStatusRunning, agentStatusSuspending:
250-
a.status = agentStatusSuspending
270+
case AgentStatusRunning, AgentStatusSuspending:
271+
a.status = AgentStatusSuspending
251272
logger.Info(ctx).Msg("Suspending agent.")
252-
case agentStatusStopping:
273+
case AgentStatusStopping:
253274
logger.Info(ctx).Msg("Stopping agent.")
254275

255276
default:
@@ -269,14 +290,14 @@ func (a *wasmAgentActor) PostStop(ac *goakt.Context) error {
269290

270291
duration := time.Since(start)
271292
switch a.status {
272-
case agentStatusSuspending:
273-
a.status = agentStatusSuspended
293+
case AgentStatusSuspending:
294+
a.status = AgentStatusSuspended
274295
if err := a.saveState(ctx); err != nil {
275296
return err
276297
}
277298
logger.Info(ctx).Msg("Agent suspended successfully.")
278-
case agentStatusStopping:
279-
a.status = agentStatusTerminated
299+
case AgentStatusStopping:
300+
a.status = AgentStatusTerminated
280301
if err := a.saveState(ctx); err != nil {
281302
return err
282303
}
@@ -388,7 +409,7 @@ func (a *wasmAgentActor) activateAgent(ctx context.Context) error {
388409
params := map[string]any{
389410
"name": a.agentName,
390411
"id": a.agentId,
391-
"reloading": a.status == agentStatusResuming,
412+
"reloading": a.status == AgentStatusResuming,
392413
}
393414

394415
execInfo, err := a.host.CallFunctionInModule(ctx, a.module, a.buffers, fnInfo, params)
@@ -405,7 +426,7 @@ func (a *wasmAgentActor) shutdownAgent(ctx context.Context) error {
405426
}
406427

407428
params := map[string]any{
408-
"suspending": a.status == agentStatusSuspending,
429+
"suspending": a.status == AgentStatusSuspending,
409430
}
410431

411432
execInfo, err := a.host.CallFunctionInModule(ctx, a.module, a.buffers, fnInfo, params)
@@ -469,7 +490,7 @@ func (a *wasmAgentActor) reloadModule(ctx context.Context, plugin *plugins.Plugi
469490

470491
logger.Info(ctx).Msg("Reloading module for agent.")
471492

472-
a.status = agentStatusSuspending
493+
a.status = AgentStatusSuspending
473494
if err := a.shutdownAgent(ctx); err != nil {
474495
logger.Err(ctx, err).Msg("Error shutting down agent.")
475496
return err
@@ -482,7 +503,7 @@ func (a *wasmAgentActor) reloadModule(ctx context.Context, plugin *plugins.Plugi
482503
return err
483504
}
484505
a.module.Close(ctx)
485-
a.status = agentStatusSuspended
506+
a.status = AgentStatusSuspended
486507

487508
// create a new module instance and assign it to the actor
488509
a.plugin = plugin
@@ -494,7 +515,7 @@ func (a *wasmAgentActor) reloadModule(ctx context.Context, plugin *plugins.Plugi
494515
a.module = mod
495516

496517
// activate the agent in the new module instance
497-
a.status = agentStatusResuming
518+
a.status = AgentStatusResuming
498519
if err := a.activateAgent(ctx); err != nil {
499520
logger.Err(ctx, err).Msg("Error reloading agent.")
500521
return err
@@ -506,7 +527,7 @@ func (a *wasmAgentActor) reloadModule(ctx context.Context, plugin *plugins.Plugi
506527
return err
507528
}
508529

509-
a.status = agentStatusRunning
530+
a.status = AgentStatusRunning
510531
logger.Info(ctx).Msg("Agent reloaded module successfully.")
511532

512533
return nil

runtime/httpserver/health.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,44 @@
1010
package httpserver
1111

1212
import (
13+
"encoding/json"
14+
"fmt"
1315
"net/http"
1416

17+
"github.com/hypermodeinc/modus/runtime/actors"
1518
"github.com/hypermodeinc/modus/runtime/app"
1619
"github.com/hypermodeinc/modus/runtime/utils"
1720
)
1821

1922
var healthHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2023
env := app.Config().Environment()
2124
ver := app.VersionNumber()
25+
agents := actors.ListAgents()
26+
27+
// custom format the JSON response for easy readability
28+
2229
w.WriteHeader(http.StatusOK)
2330
utils.WriteJsonContentHeader(w)
24-
_, _ = w.Write([]byte(`{"status":"ok","environment":"` + env + `","version":"` + ver + `"}`))
31+
_, _ = w.Write([]byte(`{
32+
"status": "ok",
33+
"environment": "` + env + `",
34+
"version": "` + ver + `",
35+
`))
36+
37+
if len(agents) == 0 {
38+
_, _ = w.Write([]byte(` "agents": []` + "\n"))
39+
} else {
40+
_, _ = w.Write([]byte(` "agents": [` + "\n"))
41+
for i, agent := range agents {
42+
if i > 0 {
43+
_, _ = w.Write([]byte(",\n"))
44+
}
45+
name, _ := json.Marshal(agent.Name)
46+
_, _ = w.Write(fmt.Appendf(nil, ` {"id": "%s", "name": %s, "status": "%s"}`, agent.Id, name, agent.Status))
47+
}
48+
_, _ = w.Write([]byte("\n ]\n"))
49+
}
50+
51+
_, _ = w.Write([]byte("}\n"))
52+
2553
})

0 commit comments

Comments
 (0)