2026-03-16 11:10:33 +00:00
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"time"
2026-03-22 03:41:07 +00:00
core "dappco.re/go/core"
2026-03-16 11:10:33 +00:00
"github.com/modelcontextprotocol/go-sdk/mcp"
)
2026-03-30 22:30:05 +00:00
// result := ReadStatusResult(workspaceDir)
// if result.OK && result.Value.(*WorkspaceStatus).Status == "completed" { autoCreatePR(workspaceDir) }
2026-03-16 11:10:33 +00:00
type WorkspaceStatus struct {
2026-03-31 05:28:26 +00:00
Status string ` json:"status" `
Agent string ` json:"agent" `
Repo string ` json:"repo" `
Org string ` json:"org,omitempty" `
Task string ` json:"task" `
Branch string ` json:"branch,omitempty" `
Issue int ` json:"issue,omitempty" `
PID int ` json:"pid,omitempty" `
ProcessID string ` json:"process_id,omitempty" `
StartedAt time . Time ` json:"started_at" `
UpdatedAt time . Time ` json:"updated_at" `
Question string ` json:"question,omitempty" `
Runs int ` json:"runs" `
PRURL string ` json:"pr_url,omitempty" `
2026-03-16 11:10:33 +00:00
}
2026-03-30 22:30:05 +00:00
// r := c.QUERY(agentic.WorkspaceQuery{})
// if r.OK { reg := r.Value.(*core.Registry[*WorkspaceStatus]) }
// r := c.QUERY(agentic.WorkspaceQuery{Name: "core/go-io/task-5"})
2026-03-26 06:38:02 +00:00
type WorkspaceQuery struct {
2026-03-31 05:28:26 +00:00
Name string
Status string
2026-03-26 06:38:02 +00:00
}
2026-03-30 21:22:54 +00:00
func writeStatus ( workspaceDir string , status * WorkspaceStatus ) error {
r := writeStatusResult ( workspaceDir , status )
2026-03-30 07:30:42 +00:00
if ! r . OK {
err , _ := r . Value . ( error )
if err == nil {
err = core . E ( "writeStatus" , "failed to write status" , nil )
}
return err
}
return nil
}
2026-03-30 22:30:05 +00:00
// result := writeStatusResult("/srv/core/workspace/core/go-io/task-5", &WorkspaceStatus{Status: "running"})
// if result.OK { return }
2026-03-30 21:22:54 +00:00
func writeStatusResult ( workspaceDir string , status * WorkspaceStatus ) core . Result {
2026-03-30 07:30:42 +00:00
if status == nil {
return core . Result { Value : core . E ( "writeStatus" , "status is required" , nil ) , OK : false }
}
2026-03-16 11:10:33 +00:00
status . UpdatedAt = time . Now ( )
2026-03-30 21:22:54 +00:00
statusPath := WorkspaceStatusPath ( workspaceDir )
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
if r := fs . WriteAtomic ( statusPath , core . JSONMarshalString ( status ) ) ; ! r . OK {
2026-03-22 03:45:50 +00:00
err , _ := r . Value . ( error )
2026-03-30 07:30:42 +00:00
if err == nil {
2026-03-30 18:59:41 +00:00
core . Warn ( "agentic.writeStatus: failed to write status" , "path" , statusPath )
2026-03-30 07:30:42 +00:00
return core . Result { Value : core . E ( "writeStatus" , "failed to write status" , nil ) , OK : false }
}
2026-03-30 18:59:41 +00:00
core . Warn ( "agentic.writeStatus: failed to write status" , "path" , statusPath , "reason" , err )
2026-03-30 07:30:42 +00:00
return core . Result { Value : core . E ( "writeStatus" , "failed to write status" , err ) , OK : false }
2026-03-22 03:41:07 +00:00
}
2026-03-30 07:30:42 +00:00
return core . Result { OK : true }
2026-03-16 11:10:33 +00:00
}
2026-03-30 22:30:05 +00:00
// result := ReadStatusResult("/path/to/workspace")
// if result.OK { workspaceStatus := result.Value.(*WorkspaceStatus) }
2026-03-30 21:22:54 +00:00
func ReadStatusResult ( workspaceDir string ) core . Result {
r := fs . Read ( WorkspaceStatusPath ( workspaceDir ) )
2026-03-22 03:41:07 +00:00
if ! r . OK {
2026-03-30 07:30:42 +00:00
err , _ := r . Value . ( error )
if err == nil {
2026-03-30 16:24:06 +00:00
return core . Result { Value : core . E ( "ReadStatusResult" , "status not found" , nil ) , OK : false }
2026-03-30 07:30:42 +00:00
}
2026-03-30 21:22:54 +00:00
return core . Result { Value : core . E ( "ReadStatusResult" , core . Concat ( "status not found for " , workspaceDir ) , err ) , OK : false }
2026-03-16 11:10:33 +00:00
}
2026-03-31 18:48:21 +00:00
var workspaceStatus WorkspaceStatus
if parseResult := core . JSONUnmarshalString ( r . Value . ( string ) , & workspaceStatus ) ; ! parseResult . OK {
2026-03-30 20:58:12 +00:00
err , _ := parseResult . Value . ( error )
2026-03-30 07:30:42 +00:00
if err == nil {
2026-03-30 16:24:06 +00:00
return core . Result { Value : core . E ( "ReadStatusResult" , "invalid status json" , nil ) , OK : false }
2026-03-30 07:30:42 +00:00
}
2026-03-30 16:24:06 +00:00
return core . Result { Value : core . E ( "ReadStatusResult" , "invalid status json" , err ) , OK : false }
2026-03-16 11:10:33 +00:00
}
2026-03-31 18:48:21 +00:00
return core . Result { Value : & workspaceStatus , OK : true }
2026-03-16 11:10:33 +00:00
}
2026-04-01 13:20:05 +00:00
// read, err := ReadStatus("/path/to/workspace")
// if err == nil { core.Println(read.Status) }
func ReadStatus ( workspaceDir string ) ( * WorkspaceStatus , error ) {
result := ReadStatusResult ( workspaceDir )
if ! result . OK {
err , _ := result . Value . ( error )
if err == nil {
err = core . E ( "ReadStatus" , "failed to read status" , nil )
}
return nil , err
}
workspaceStatus , ok := workspaceStatusValue ( result )
if ! ok {
return nil , core . E ( "ReadStatus" , "invalid status payload" , nil )
}
return workspaceStatus , nil
}
2026-03-30 22:30:05 +00:00
// result := ReadStatusResult("/path/to/workspace")
// workspaceStatus, ok := workspaceStatusValue(result)
2026-03-30 19:40:02 +00:00
func workspaceStatusValue ( result core . Result ) ( * WorkspaceStatus , bool ) {
2026-03-30 20:58:12 +00:00
workspaceStatus , ok := result . Value . ( * WorkspaceStatus )
if ! ok || workspaceStatus == nil {
2026-03-30 19:40:02 +00:00
return nil , false
}
2026-03-30 20:58:12 +00:00
return workspaceStatus , true
2026-03-30 19:40:02 +00:00
}
2026-03-31 18:55:26 +00:00
// input := agentic.StatusInput{Workspace: "core/go-io/task-42", Status: "blocked", Limit: 50}
2026-03-16 11:10:33 +00:00
type StatusInput struct {
2026-03-31 05:28:26 +00:00
Workspace string ` json:"workspace,omitempty" `
Limit int ` json:"limit,omitempty" `
Status string ` json:"status,omitempty" `
2026-03-16 11:10:33 +00:00
}
2026-03-30 22:30:05 +00:00
// out := agentic.StatusOutput{Total: 42, Running: 3, Queued: 10, Completed: 25}
2026-03-16 11:10:33 +00:00
type StatusOutput struct {
2026-03-29 20:15:58 +00:00
Total int ` json:"total" `
Running int ` json:"running" `
Queued int ` json:"queued" `
Completed int ` json:"completed" `
Failed int ` json:"failed" `
Blocked [ ] BlockedInfo ` json:"blocked,omitempty" `
2026-03-16 11:10:33 +00:00
}
2026-03-30 22:30:05 +00:00
// info := agentic.BlockedInfo{Name: "core/go-io/task-4", Repo: "go-io", Question: "Which API version?"}
2026-03-23 12:53:33 +00:00
type BlockedInfo struct {
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
Name string ` json:"name" `
Repo string ` json:"repo" `
2026-03-23 12:53:33 +00:00
Agent string ` json:"agent" `
Question string ` json:"question" `
2026-03-16 11:10:33 +00:00
}
func ( s * PrepSubsystem ) registerStatusTool ( server * mcp . Server ) {
mcp . AddTool ( server , & mcp . Tool {
Name : "agentic_status" ,
2026-03-31 18:55:26 +00:00
Description : "List agent workspaces and their status (running, completed, blocked, failed). Supports workspace, status, and limit filters. Shows blocked agents with their questions." ,
2026-03-16 11:10:33 +00:00
} , s . status )
}
func ( s * PrepSubsystem ) status ( ctx context . Context , _ * mcp . CallToolRequest , input StatusInput ) ( * mcp . CallToolResult , StatusOutput , error ) {
2026-03-29 20:15:58 +00:00
statusFiles := WorkspaceStatusPaths ( )
2026-03-30 16:01:32 +00:00
var runtime * core . Core
if s . ServiceRuntime != nil {
runtime = s . Core ( )
}
2026-03-16 11:10:33 +00:00
2026-03-31 18:48:21 +00:00
var statusSummary StatusOutput
2026-03-31 18:55:26 +00:00
matched := 0
2026-03-16 11:10:33 +00:00
2026-03-22 16:03:11 +00:00
for _ , statusPath := range statusFiles {
2026-03-30 21:22:54 +00:00
workspaceDir := core . PathDir ( statusPath )
name := WorkspaceName ( workspaceDir )
2026-03-31 18:55:26 +00:00
if ! statusInputMatchesWorkspace ( input . Workspace , workspaceDir , name ) {
continue
}
2026-03-16 11:10:33 +00:00
2026-03-30 21:22:54 +00:00
result := ReadStatusResult ( workspaceDir )
2026-03-30 20:58:12 +00:00
workspaceStatus , ok := workspaceStatusValue ( result )
2026-03-30 19:40:02 +00:00
if ! ok {
2026-03-31 18:55:26 +00:00
if input . Status != "" && input . Status != "failed" {
continue
}
if ! statusInputMatchesStatus ( input . Status , "failed" ) {
continue
}
2026-03-31 18:48:21 +00:00
statusSummary . Total ++
statusSummary . Failed ++
2026-03-31 18:55:26 +00:00
matched ++
if input . Limit > 0 && matched >= input . Limit {
break
}
2026-03-16 11:10:33 +00:00
continue
}
2026-03-30 20:58:12 +00:00
if workspaceStatus . Status == "running" && ( workspaceStatus . ProcessID != "" || workspaceStatus . PID > 0 ) {
if ! ProcessAlive ( runtime , workspaceStatus . ProcessID , workspaceStatus . PID ) {
2026-03-30 21:22:54 +00:00
blockedPath := workspaceBlockedPath ( workspaceDir )
2026-03-22 03:41:07 +00:00
if r := fs . Read ( blockedPath ) ; r . OK {
2026-03-30 20:58:12 +00:00
workspaceStatus . Status = "blocked"
workspaceStatus . Question = core . Trim ( r . Value . ( string ) )
2026-03-16 11:10:33 +00:00
} else {
2026-03-30 21:22:54 +00:00
if len ( workspaceLogFiles ( workspaceDir ) ) == 0 {
2026-03-30 20:58:12 +00:00
workspaceStatus . Status = "failed"
workspaceStatus . Question = "Agent process died (no output log)"
2026-03-21 17:10:43 +00:00
} else {
2026-03-30 20:58:12 +00:00
workspaceStatus . Status = "completed"
2026-03-21 17:10:43 +00:00
}
2026-03-16 11:10:33 +00:00
}
2026-03-30 21:22:54 +00:00
writeStatusResult ( workspaceDir , workspaceStatus )
2026-03-16 11:10:33 +00:00
}
}
2026-03-31 18:55:26 +00:00
if ! statusInputMatchesStatus ( input . Status , workspaceStatus . Status ) {
continue
}
2026-03-31 18:48:21 +00:00
statusSummary . Total ++
2026-03-30 20:58:12 +00:00
switch workspaceStatus . Status {
2026-03-23 12:53:33 +00:00
case "running" :
2026-03-31 18:48:21 +00:00
statusSummary . Running ++
2026-03-23 12:53:33 +00:00
case "queued" :
2026-03-31 18:48:21 +00:00
statusSummary . Queued ++
2026-03-23 12:53:33 +00:00
case "completed" :
2026-03-31 18:48:21 +00:00
statusSummary . Completed ++
2026-03-23 12:53:33 +00:00
case "failed" :
2026-03-31 18:48:21 +00:00
statusSummary . Failed ++
2026-03-23 12:53:33 +00:00
case "blocked" :
2026-03-31 18:48:21 +00:00
statusSummary . Blocked = append ( statusSummary . Blocked , BlockedInfo {
2026-03-23 12:53:33 +00:00
Name : name ,
2026-03-30 20:58:12 +00:00
Repo : workspaceStatus . Repo ,
Agent : workspaceStatus . Agent ,
Question : workspaceStatus . Question ,
2026-03-23 12:53:33 +00:00
} )
2026-03-16 11:10:33 +00:00
}
2026-03-31 18:55:26 +00:00
matched ++
if input . Limit > 0 && matched >= input . Limit {
break
}
2026-03-16 11:10:33 +00:00
}
2026-03-31 18:48:21 +00:00
return nil , statusSummary , nil
2026-03-16 11:10:33 +00:00
}
2026-03-31 18:55:26 +00:00
func statusInputMatchesWorkspace ( requested , workspaceDir , workspaceName string ) bool {
if requested == "" {
return true
}
if requested == workspaceName {
return true
}
if requested == workspaceDir {
return true
}
return false
}
func statusInputMatchesStatus ( requested , current string ) bool {
if requested == "" {
return true
}
return requested == current
}