Plan 1: Critical bug fixes (v0.7.1, zero breakage) - ACTION chain, panic recovery, defer shutdown, stale code removal Plan 2: Registry[T] primitive (Section 20) - Foundation brick, migration of 5 internal registries Plan 3: Action/Task system (Section 18) - Named callables, task composition, cascade fix Plan 4: c.Process() primitive (Section 17) - go-process v0.7.0, proc.go migration, atomic writes Plan 5: Missing primitives + AX-7 - core.ID(), ValidateName, WriteAtomic, RunE(), test coverage Plan 6: Ecosystem sweep (Phase 3) - 44 repos in 5 batches, Codex dispatch with RFC as spec Each plan lists: files to change, code examples, what it resolves, dependencies on other plans, and migration strategy. Co-Authored-By: Virgil <virgil@lethean.io>
98 lines
2.4 KiB
Markdown
98 lines
2.4 KiB
Markdown
# Implementation Plan 3 — Action/Task System (Section 18)
|
||
|
||
> Depends on: Plan 2 (Registry). The execution primitive.
|
||
|
||
## Phase A: Action Registry
|
||
|
||
**New file:** `action.go` (renamed from `task.go`)
|
||
|
||
```go
|
||
type ActionDef struct {
|
||
Name string
|
||
Handler ActionHandler
|
||
Description string
|
||
Schema Options // expected input keys
|
||
enabled bool // for Disable()
|
||
}
|
||
|
||
type ActionHandler func(context.Context, Options) Result
|
||
```
|
||
|
||
**Core accessor:**
|
||
|
||
```go
|
||
// Dual-purpose (like Service):
|
||
c.Action("process.run", handler) // register
|
||
c.Action("process.run").Run(ctx, opts) // invoke
|
||
c.Action("process.run").Exists() // check
|
||
c.Action("process.run").Def() // metadata
|
||
```
|
||
|
||
Internally uses `Registry[*ActionDef]`.
|
||
|
||
## Phase B: Move Registration to IPC
|
||
|
||
Move from `task.go`:
|
||
- `RegisterAction` → `ipc.go` (registers in `Registry[ActionHandler]`)
|
||
- `RegisterActions` → `ipc.go`
|
||
- `RegisterTask` → `ipc.go`
|
||
|
||
Keep in `action.go`:
|
||
- `Perform` → becomes `c.Action("name").Run()`
|
||
- `PerformAsync` → becomes `c.Action("name").RunAsync()`
|
||
- `Progress` → stays
|
||
|
||
## Phase C: Panic Recovery on All Actions
|
||
|
||
Every `Action.Run()` wraps in recover:
|
||
|
||
```go
|
||
func (a *ActionDef) Run(ctx context.Context, opts Options) (result Result) {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
result = Result{E("action.Run", Sprint("panic: ", r), nil), false}
|
||
}
|
||
}()
|
||
return a.Handler(ctx, opts)
|
||
}
|
||
```
|
||
|
||
## Phase D: Task Composition (v0.8.0 stretch)
|
||
|
||
```go
|
||
type TaskDef struct {
|
||
Name string
|
||
Steps []Step
|
||
}
|
||
|
||
type Step struct {
|
||
Action string
|
||
With Options
|
||
Async bool // run in background
|
||
Input string // "previous" = output of last step
|
||
}
|
||
```
|
||
|
||
`c.Task("name", def)` registers. `c.Task("name").Run(ctx)` executes steps in order.
|
||
|
||
## Resolves
|
||
|
||
I16 (task.go concerns), P6-1 (cascade → Task pipeline), P6-2 (O(N×M) → direct dispatch), P7-3 (panic recovery), P7-8 (circuit breaker via Disable), P10-2 (PERFORM not ACTION for request/response).
|
||
|
||
## Migration in core/agent
|
||
|
||
After Actions exist, refactor `handlers.go`:
|
||
|
||
```go
|
||
// Current: nested c.ACTION() cascade
|
||
// Target:
|
||
c.Task("agent.completion", TaskDef{
|
||
Steps: []Step{
|
||
{Action: "agentic.qa"},
|
||
{Action: "agentic.auto-pr"},
|
||
{Action: "agentic.verify"},
|
||
{Action: "agentic.ingest", Async: true},
|
||
{Action: "agentic.poke", Async: true},
|
||
},
|
||
})
|
||
```
|