fix(ax): align runner helper layer and examples

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-29 21:27:32 +00:00
parent 4cc763176f
commit e8a46c2f95
6 changed files with 221 additions and 19 deletions

View file

@ -3,41 +3,43 @@
package runner
import (
"time"
"dappco.re/go/agent/pkg/agentic"
core "dappco.re/go/core"
)
// fs is the file I/O medium for the runner package.
var fs = (&core.Fs{}).NewUnrestricted()
// fs reuses the shared unrestricted filesystem used by agentic.
var fs = agentic.LocalFs()
// WorkspaceRoot returns the root directory for agent workspaces.
//
// root := runner.WorkspaceRoot() // ~/Code/.core/workspace
func WorkspaceRoot() string {
return core.JoinPath(CoreRoot(), "workspace")
return agentic.WorkspaceRoot()
}
// CoreRoot returns the root directory for core ecosystem files.
//
// root := runner.CoreRoot() // ~/Code/.core
func CoreRoot() string {
if root := core.Env("CORE_WORKSPACE"); root != "" {
return root
}
return core.JoinPath(core.Env("DIR_HOME"), "Code", ".core")
return agentic.CoreRoot()
}
// ReadStatus reads a workspace status.json.
//
// st, err := runner.ReadStatus("/path/to/workspace")
func ReadStatus(wsDir string) (*WorkspaceStatus, error) {
path := core.JoinPath(wsDir, "status.json")
r := fs.Read(path)
if !r.OK {
return nil, core.E("runner.ReadStatus", "failed to read status", nil)
status, err := agentic.ReadStatus(wsDir)
if err != nil {
return nil, core.E("runner.ReadStatus", "failed to read status", err)
}
json := core.JSONMarshalString(status)
var st WorkspaceStatus
if result := core.JSONUnmarshalString(r.Value.(string), &st); !result.OK {
return nil, core.E("runner.ReadStatus", "failed to parse status", nil)
if result := core.JSONUnmarshalString(json, &st); !result.OK {
parseErr, _ := result.Value.(error)
return nil, core.E("runner.ReadStatus", "failed to parse status", parseErr)
}
return &st, nil
}
@ -46,6 +48,15 @@ func ReadStatus(wsDir string) (*WorkspaceStatus, error) {
//
// runner.WriteStatus(wsDir, &runner.WorkspaceStatus{Status: "running", Agent: "codex"})
func WriteStatus(wsDir string, st *WorkspaceStatus) {
path := core.JoinPath(wsDir, "status.json")
fs.Write(path, core.JSONMarshalString(st))
if st == nil {
return
}
json := core.JSONMarshalString(st)
var status agentic.WorkspaceStatus
if result := core.JSONUnmarshalString(json, &status); !result.OK {
return
}
status.UpdatedAt = time.Now()
fs.WriteAtomic(agentic.WorkspaceStatusPath(wsDir), core.JSONMarshalString(&status))
}

View file

@ -0,0 +1,38 @@
// SPDX-License-Identifier: EUPL-1.2
package runner
import (
core "dappco.re/go/core"
)
func ExampleCoreRoot() {
root := CoreRoot()
core.Println(core.HasSuffix(root, ".core"))
// Output: true
}
func ExampleWorkspaceRoot() {
root := WorkspaceRoot()
core.Println(core.HasSuffix(root, "workspace"))
// Output: true
}
func ExampleWriteStatus() {
fsys := (&core.Fs{}).NewUnrestricted()
dir := fsys.TempDir("runner-paths")
defer fsys.DeleteAll(dir)
WriteStatus(dir, &WorkspaceStatus{
Status: "running",
Agent: "codex",
Repo: "go-io",
})
st, err := ReadStatus(dir)
core.Println(err == nil)
core.Println(st.Status)
// Output:
// true
// running
}

107
pkg/runner/paths_test.go Normal file
View file

@ -0,0 +1,107 @@
// SPDX-License-Identifier: EUPL-1.2
package runner
import (
"testing"
"time"
"dappco.re/go/agent/pkg/agentic"
core "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPaths_CoreRoot_Good_EnvVar(t *testing.T) {
t.Setenv("CORE_WORKSPACE", "/tmp/core-root")
assert.Equal(t, "/tmp/core-root", CoreRoot())
}
func TestPaths_CoreRoot_Bad_Fallback(t *testing.T) {
t.Setenv("CORE_WORKSPACE", "")
home := core.Env("DIR_HOME")
assert.Equal(t, home+"/Code/.core", CoreRoot())
}
func TestPaths_CoreRoot_Ugly_UnicodePath(t *testing.T) {
t.Setenv("CORE_WORKSPACE", "/tmp/core-røot")
assert.Equal(t, "/tmp/core-røot", CoreRoot())
}
func TestPaths_WorkspaceRoot_Good(t *testing.T) {
t.Setenv("CORE_WORKSPACE", "/tmp/core-root")
assert.Equal(t, "/tmp/core-root/workspace", WorkspaceRoot())
}
func TestPaths_WorkspaceRoot_Bad_EmptyEnv(t *testing.T) {
t.Setenv("CORE_WORKSPACE", "")
home := core.Env("DIR_HOME")
assert.Equal(t, home+"/Code/.core/workspace", WorkspaceRoot())
}
func TestPaths_WorkspaceRoot_Ugly_NestedCoreRoot(t *testing.T) {
t.Setenv("CORE_WORKSPACE", "/srv/core/tenant-a")
assert.Equal(t, "/srv/core/tenant-a/workspace", WorkspaceRoot())
}
func TestPaths_ReadStatus_Good_AgenticShape(t *testing.T) {
wsDir := t.TempDir()
status := &agentic.WorkspaceStatus{
Status: "completed",
Agent: "codex",
Repo: "go-io",
Task: "Finish AX cleanup",
Branch: "agent/ax-cleanup",
PID: 42,
ProcessID: "proc-123",
StartedAt: time.Now(),
UpdatedAt: time.Now(),
Question: "Ready?",
Runs: 2,
PRURL: "https://forge.test/core/go-io/pulls/12",
}
require.True(t, agentic.LocalFs().WriteAtomic(agentic.WorkspaceStatusPath(wsDir), core.JSONMarshalString(status)).OK)
st, err := ReadStatus(wsDir)
require.NoError(t, err)
assert.Equal(t, "completed", st.Status)
assert.Equal(t, "codex", st.Agent)
assert.Equal(t, "go-io", st.Repo)
assert.Equal(t, "agent/ax-cleanup", st.Branch)
assert.Equal(t, 2, st.Runs)
}
func TestPaths_ReadStatus_Bad_InvalidJSON(t *testing.T) {
wsDir := t.TempDir()
require.True(t, agentic.LocalFs().WriteAtomic(agentic.WorkspaceStatusPath(wsDir), "{not-json").OK)
_, err := ReadStatus(wsDir)
assert.Error(t, err)
}
func TestPaths_WriteStatus_Ugly_AtomicOverwrite(t *testing.T) {
wsDir := t.TempDir()
WriteStatus(wsDir, &WorkspaceStatus{
Status: "running",
Agent: "codex",
Repo: "go-io",
Task: "First run",
})
WriteStatus(wsDir, &WorkspaceStatus{
Status: "completed",
Agent: "claude",
Repo: "go-io",
Task: "Second run",
Branch: "agent/ax-cleanup",
StartedAt: time.Now(),
Runs: 3,
})
st, err := ReadStatus(wsDir)
require.NoError(t, err)
assert.Equal(t, "completed", st.Status)
assert.Equal(t, "claude", st.Agent)
assert.Equal(t, "agent/ax-cleanup", st.Branch)
assert.Equal(t, 3, st.Runs)
}

View file

@ -0,0 +1,23 @@
// SPDX-License-Identifier: EUPL-1.2
package runner
import (
core "dappco.re/go/core"
"gopkg.in/yaml.v3"
)
func ExampleConcurrencyLimit_UnmarshalYAML() {
input := `
total: 5
gpt-5.4: 1
`
var limit ConcurrencyLimit
_ = yaml.Unmarshal([]byte(input), &limit)
core.Println(limit.Total)
core.Println(limit.Models["gpt-5.4"])
// Output:
// 5
// 1
}

View file

@ -25,8 +25,8 @@ type Options struct{}
// Manages concurrency limits, queue drain, workspace lifecycle, and frozen state.
// All dispatch requests — MCP tool, CLI, or IPC — go through this service.
//
// r := runner.New()
// r.Dispatch(ctx, input) // checks frozen + concurrency, spawns or queues
// svc := runner.New()
// svc.TrackWorkspace("core/go-io/task-5", &runner.WorkspaceStatus{Status: "running", Agent: "codex"})
type Service struct {
*core.ServiceRuntime[Options]
dispatchMu sync.Mutex
@ -77,8 +77,11 @@ func Register(c *core.Core) core.Result {
// OnStartup registers Actions and starts the queue runner.
//
// c.Perform("runner.dispatch", opts) // dispatch an agent
// c.Perform("runner.status", opts) // query workspace status
// c.Action("runner.dispatch").Run(ctx, core.NewOptions(
// core.Option{Key: "repo", Value: "go-io"},
// core.Option{Key: "agent", Value: "codex"},
// ))
// c.Action("runner.status").Run(ctx, core.NewOptions())
func (s *Service) OnStartup(ctx context.Context) core.Result {
c := s.Core()

View file

@ -0,0 +1,20 @@
// SPDX-License-Identifier: EUPL-1.2
package runner
import (
core "dappco.re/go/core"
)
func ExampleNew() {
svc := New()
core.Println(svc.Workspaces().Len())
// Output: 0
}
func ExampleRegister() {
c := core.New(core.WithOption("name", "runner-example"))
r := Register(c)
core.Println(r.OK)
// Output: true
}