Skip to content
Closed
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
2 changes: 2 additions & 0 deletions backend/internal/adapters/agent/activitydispatch/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/copilot"
"github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/cursor"
"github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/droid"
"github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/goose"
"github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/opencode"
"github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/qwen"
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
Expand Down Expand Up @@ -43,6 +44,7 @@ var Derivers = map[string]DeriveFunc{
"cursor": cursor.DeriveActivityState,
"qwen": qwen.DeriveActivityState,
"copilot": copilot.DeriveActivityState,
"goose": goose.DeriveActivityState,
}

// Derive looks up the deriver for an agent token and applies it. ok=false when
Expand Down
35 changes: 35 additions & 0 deletions backend/internal/adapters/agent/goose/activity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package goose

import "github.com/aoagents/agent-orchestrator/backend/internal/domain"

// DeriveActivityState maps a Goose hook event onto an AO activity state. The
// bool is false when the event carries no activity signal.
//
// event is the AO hook sub-command name installed in gooseManagedHooks
// ("session-start", "user-prompt-submit", "stop", "permission-request"), not
// the native Goose event name.
//
// Goose's native hook surface (as of 2026-05) emits SessionStart /
// UserPromptSubmit / Stop / SessionEnd plus the tool-use events, but has no
// dedicated permission/approval event yet, so AO does not install a
// "permission-request" hook today. The case is kept here so that, if a future
// Goose release adds an approval lifecycle event, mapping it to waiting_input is
// a one-line hooks.go change with no deriver edit needed.
//
// TODO(goose): ActivityExited is still runtime-observation-owned. Goose has a
// native SessionEnd hook; if AO starts installing it, map it to ActivityExited
// here. Until then, the lifecycle reaper marks a dead Goose runtime as exited.
func DeriveActivityState(event string, _ []byte) (domain.ActivityState, bool) {
switch event {
case "session-start":
return domain.ActivityActive, true
case "user-prompt-submit":
return domain.ActivityActive, true
case "stop":
return domain.ActivityIdle, true
case "permission-request":
return domain.ActivityWaitingInput, true
default:
return "", false
}
}
32 changes: 32 additions & 0 deletions backend/internal/adapters/agent/goose/activity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package goose

import (
"testing"

"github.com/aoagents/agent-orchestrator/backend/internal/domain"
)

func TestDeriveActivityState(t *testing.T) {
tests := []struct {
name string
event string
want domain.ActivityState
wantOK bool
}{
{"session start -> active", "session-start", domain.ActivityActive, true},
{"user prompt -> active", "user-prompt-submit", domain.ActivityActive, true},
{"stop -> idle", "stop", domain.ActivityIdle, true},
{"permission request -> waiting input", "permission-request", domain.ActivityWaitingInput, true},
{"unknown event -> no signal", "frobnicate", "", false},
{"empty event -> no signal", "", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := DeriveActivityState(tt.event, []byte(`{}`))
if got != tt.want || ok != tt.wantOK {
t.Fatalf("DeriveActivityState(%q) = (%q, %v), want (%q, %v)",
tt.event, got, ok, tt.want, tt.wantOK)
}
})
}
}
Loading
Loading