fix: rewrite 4 stale docs — messaging, primitives, index, getting-started, testing

All PERFORM/RegisterTask/type Task any references replaced with
named Action patterns. Every code example now uses the v0.8.0 API.

- docs/messaging.md: full rewrite — ACTION/QUERY + named Actions + Task
- docs/primitives.md: full rewrite — added Action, Task, Registry, Entitlement
- docs/index.md: full rewrite — updated surface table, quick example, doc links
- docs/getting-started.md: 2 RegisterTask+PERFORM blocks → Action pattern
- docs/testing.md: 1 RegisterTask+PERFORM block → Action pattern

An agent reading any doc file now gets compilable code.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-25 17:13:27 +00:00
parent ba77e029c8
commit 1f0c618b7a
5 changed files with 230 additions and 331 deletions

View file

@ -69,20 +69,15 @@ c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result {
return core.Result{}
})
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
switch task := t.(type) {
case createWorkspaceTask:
path := "/tmp/agent-workbench/" + task.Name
return core.Result{Value: path, OK: true}
}
return core.Result{}
c.Action("workspace.create", func(_ context.Context, opts core.Options) core.Result {
name := opts.String("name")
path := "/tmp/agent-workbench/" + name
return core.Result{Value: path, OK: true}
})
c.Command("workspace/create", core.Command{
Action: func(opts core.Options) core.Result {
return c.PERFORM(createWorkspaceTask{
Name: opts.String("name"),
})
return c.Action("workspace.create").Run(context.Background(), opts)
},
})
```
@ -170,20 +165,15 @@ func main() {
return core.Result{}
})
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
switch task := t.(type) {
case createWorkspaceTask:
path := c.Config().String("workspace.root") + "/" + task.Name
return core.Result{Value: path, OK: true}
}
return core.Result{}
c.Action("workspace.create", func(_ context.Context, opts core.Options) core.Result {
name := opts.String("name")
path := c.Config().String("workspace.root") + "/" + name
return core.Result{Value: path, OK: true}
})
c.Command("workspace/create", core.Command{
Action: func(opts core.Options) core.Result {
return c.PERFORM(createWorkspaceTask{
Name: opts.String("name"),
})
return c.Action("workspace.create").Run(context.Background(), opts)
},
})

View file

@ -5,108 +5,56 @@ description: AX-first documentation for the CoreGO framework.
# CoreGO
CoreGO is the foundation layer for the Core ecosystem. It gives you one container, one command tree, one message bus, and a small set of shared primitives that repeat across the whole framework.
CoreGO is the foundation layer for the Core ecosystem. Module: `dappco.re/go/core`.
The current module path is `dappco.re/go/core`.
## What CoreGO Provides
## AX View
CoreGO already follows the main AX ideas from RFC-025:
- predictable names such as `Core`, `Service`, `Command`, `Options`, `Result`, `Message`
- path-shaped command registration such as `deploy/to/homelab`
- one repeated input shape (`Options`) and one repeated return shape (`Result`)
- comments and examples that show real usage instead of restating the type signature
## What CoreGO Owns
| Surface | Purpose |
|---------|---------|
| `Core` | Central container and access point |
| `Service` | Managed lifecycle component |
| `Command` | Path-based command tree node |
| `ACTION`, `QUERY`, `PERFORM` | Decoupled communication between components |
| `Data`, `Drive`, `Fs`, `Config`, `I18n`, `Cli` | Built-in subsystems for common runtime work |
| `E`, `Wrap`, `ErrorLog`, `ErrorPanic` | Structured failures and panic recovery |
| Primitive | Purpose |
|-----------|---------|
| `Core` | Central container — everything registers here |
| `Service` | Lifecycle-managed component (Startable/Stoppable return Result) |
| `Action` | Named callable with panic recovery + entitlement |
| `Task` | Composed sequence of Actions |
| `Registry[T]` | Thread-safe named collection (universal brick) |
| `Command` | Path-based CLI command tree |
| `Process` | Managed execution (Action sugar over go-process) |
| `API` | Remote streams (protocol handlers + Drive) |
| `Entitlement` | Permission gate (default permissive, consumer replaces) |
| `ACTION`, `QUERY` | Anonymous broadcast + request/response |
| `Data`, `Drive`, `Fs`, `Config`, `I18n` | Built-in subsystems |
## Quick Example
```go
package main
import (
"context"
"fmt"
"dappco.re/go/core"
)
type flushCacheTask struct {
Name string
}
import "dappco.re/go/core"
func main() {
c := core.New(core.Options{
{Key: "name", Value: "agent-workbench"},
})
c.Service("cache", core.Service{
OnStart: func() core.Result {
core.Info("cache ready", "app", c.App().Name)
return core.Result{OK: true}
},
OnStop: func() core.Result {
core.Info("cache stopped", "app", c.App().Name)
return core.Result{OK: true}
},
})
c.RegisterTask(func(_ *core.Core, task core.Task) core.Result {
switch task.(type) {
case flushCacheTask:
return core.Result{Value: "cache flushed", OK: true}
}
return core.Result{}
})
c.Command("cache/flush", core.Command{
Action: func(opts core.Options) core.Result {
return c.PERFORM(flushCacheTask{Name: opts.String("name")})
},
})
if !c.ServiceStartup(context.Background(), nil).OK {
panic("startup failed")
}
r := c.Cli().Run("cache", "flush", "--name=session-store")
fmt.Println(r.Value)
_ = c.ServiceShutdown(context.Background())
c := core.New(
core.WithOption("name", "agent-workbench"),
core.WithService(cache.Register),
core.WithServiceLock(),
)
c.Run()
}
```
## Documentation Paths
## API Specification
The full contract is `docs/RFC.md` (21 sections, 1476 lines). An agent should be able to write a service from RFC.md alone.
## Documentation
| Path | Covers |
|------|--------|
| [getting-started.md](getting-started.md) | First runnable CoreGO app |
| [primitives.md](primitives.md) | `Options`, `Result`, `Service`, `Message`, `Query`, `Task` |
| [services.md](services.md) | Service registry, service locks, runtime helpers |
| [commands.md](commands.md) | Path-based commands and CLI execution |
| [messaging.md](messaging.md) | `ACTION`, `QUERY`, `QUERYALL`, `PERFORM`, `PerformAsync` |
| [lifecycle.md](lifecycle.md) | Startup, shutdown, context, background task draining |
| [configuration.md](configuration.md) | Constructor options, config state, feature flags |
| [subsystems.md](subsystems.md) | `App`, `Data`, `Drive`, `Fs`, `I18n`, `Cli` |
| [errors.md](errors.md) | Structured errors, logging helpers, panic recovery |
| [testing.md](testing.md) | Test naming and framework-level testing patterns |
| [pkg/core.md](pkg/core.md) | Package-level reference summary |
| [pkg/log.md](pkg/log.md) | Logging reference for the root package |
| [pkg/PACKAGE_STANDARDS.md](pkg/PACKAGE_STANDARDS.md) | AX package-authoring guidance |
## Good Reading Order
1. Start with [getting-started.md](getting-started.md).
2. Learn the repeated shapes in [primitives.md](primitives.md).
3. Pick the integration path you need next: [services.md](services.md), [commands.md](commands.md), or [messaging.md](messaging.md).
4. Use [subsystems.md](subsystems.md), [errors.md](errors.md), and [testing.md](testing.md) as reference pages while building.
| [RFC.md](RFC.md) | Authoritative API contract (21 sections) |
| [primitives.md](primitives.md) | Option, Result, Action, Task, Registry, Entitlement |
| [services.md](services.md) | Service registry, ServiceRuntime, service locks |
| [commands.md](commands.md) | Path-based commands, Managed field |
| [messaging.md](messaging.md) | ACTION, QUERY, named Actions, PerformAsync |
| [lifecycle.md](lifecycle.md) | RunE, ServiceStartup, ServiceShutdown |
| [subsystems.md](subsystems.md) | App, Data, Drive, Fs, Config, I18n |
| [errors.md](errors.md) | core.E(), structured errors, panic recovery |
| [testing.md](testing.md) | AX-7 TestFile_Function_{Good,Bad,Ugly} |
| [configuration.md](configuration.md) | WithOption, WithService, WithServiceLock |

View file

@ -1,171 +1,127 @@
---
title: Messaging
description: ACTION, QUERY, QUERYALL, PERFORM, and async task flow.
description: ACTION, QUERY, QUERYALL, named Actions, and async dispatch.
---
# Messaging
CoreGO uses one message bus for broadcasts, lookups, and work dispatch.
CoreGO has two messaging layers: anonymous broadcast (ACTION/QUERY) and named Actions.
## Message Types
## Anonymous Broadcast
```go
type Message any
type Query any
type Task any
```
### `ACTION`
Your own structs define the protocol.
```go
type repositoryIndexed struct {
Name string
}
type repositoryCountQuery struct{}
type syncRepositoryTask struct {
Name string
}
```
## `ACTION`
`ACTION` is a broadcast.
Fire-and-forget broadcast to all registered handlers. Each handler is wrapped in panic recovery. Handler return values are ignored — all handlers fire regardless.
```go
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
switch m := msg.(type) {
case repositoryIndexed:
core.Info("repository indexed", "name", m.Name)
return core.Result{OK: true}
}
return core.Result{OK: true}
if ev, ok := msg.(repositoryIndexed); ok {
core.Info("indexed", "name", ev.Name)
}
return core.Result{OK: true}
})
r := c.ACTION(repositoryIndexed{Name: "core-go"})
c.ACTION(repositoryIndexed{Name: "core-go"})
```
### Behavior
### `QUERY`
- all registered action handlers are called in their current registration order
- if a handler returns `OK:false`, dispatch stops and that `Result` is returned
- if no handler fails, `ACTION` returns `Result{OK:true}`
## `QUERY`
`QUERY` is first-match request-response.
First handler to return `OK:true` wins.
```go
c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result {
switch q.(type) {
case repositoryCountQuery:
return core.Result{Value: 42, OK: true}
}
return core.Result{}
if _, ok := q.(repositoryCountQuery); ok {
return core.Result{Value: 42, OK: true}
}
return core.Result{}
})
r := c.QUERY(repositoryCountQuery{})
```
### Behavior
### `QUERYALL`
- handlers run until one returns `OK:true`
- the first successful result wins
- if nothing handles the query, CoreGO returns an empty `Result`
## `QUERYALL`
`QUERYALL` collects every successful non-nil response.
Collects every successful non-nil response.
```go
r := c.QUERYALL(repositoryCountQuery{})
results := r.Value.([]any)
```
### Behavior
## Named Actions
- every query handler is called
- only `OK:true` results with non-nil `Value` are collected
- the call itself returns `OK:true` even when the result list is empty
Named Actions are the typed, inspectable replacement for anonymous dispatch. See Section 18 of `RFC.md`.
## `PERFORM`
`PERFORM` dispatches a task to the first handler that accepts it.
### Register and Invoke
```go
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
switch task := t.(type) {
case syncRepositoryTask:
return core.Result{Value: "synced " + task.Name, OK: true}
}
return core.Result{}
// Register during OnStartup
c.Action("repo.sync", func(ctx context.Context, opts core.Options) core.Result {
name := opts.String("name")
return core.Result{Value: "synced " + name, OK: true}
})
r := c.PERFORM(syncRepositoryTask{Name: "core-go"})
// Invoke by name
r := c.Action("repo.sync").Run(ctx, core.NewOptions(
core.Option{Key: "name", Value: "core-go"},
))
```
### Behavior
- handlers run until one returns `OK:true`
- the first successful result wins
- if nothing handles the task, CoreGO returns an empty `Result`
## `PerformAsync`
`PerformAsync` runs a task in a background goroutine and returns a generated task identifier.
### Capability Check
```go
r := c.PerformAsync(syncRepositoryTask{Name: "core-go"})
taskID := r.Value.(string)
if c.Action("process.run").Exists() {
// go-process is registered
}
c.Actions() // []string of all registered action names
```
### Generated Events
### Permission Gate
Async execution emits three action messages:
Every `Action.Run()` checks `c.Entitled(action.Name)` before executing. See Section 21 of `RFC.md`.
| Message | When |
|---------|------|
| `ActionTaskStarted` | just before background execution begins |
| `ActionTaskProgress` | whenever `Progress` is called |
| `ActionTaskCompleted` | after the task finishes or panics |
## Task Composition
Example listener:
A Task is a named sequence of Actions:
```go
c.Task("deploy", core.Task{
Steps: []core.Step{
{Action: "go.build"},
{Action: "go.test"},
{Action: "docker.push"},
{Action: "notify.slack", Async: true},
},
})
r := c.Task("deploy").Run(ctx, c, opts)
```
Sequential steps stop on first failure. `Async: true` steps fire without blocking. `Input: "previous"` pipes output.
## Background Execution
```go
r := c.PerformAsync("repo.sync", opts)
taskID := r.Value.(string)
c.Progress(taskID, 0.5, "indexing commits", "repo.sync")
```
Broadcasts `ActionTaskStarted`, `ActionTaskProgress`, `ActionTaskCompleted` as ACTION messages.
### Completion Listener
```go
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
switch m := msg.(type) {
case core.ActionTaskCompleted:
core.Info("task completed", "task", m.TaskIdentifier, "err", m.Error)
}
return core.Result{OK: true}
if ev, ok := msg.(core.ActionTaskCompleted); ok {
core.Info("done", "task", ev.TaskIdentifier, "ok", ev.Result.OK)
}
return core.Result{OK: true}
})
```
## Progress Updates
## Shutdown
```go
c.Progress(taskID, 0.5, "indexing commits", syncRepositoryTask{Name: "core-go"})
```
That broadcasts `ActionTaskProgress`.
## `TaskWithIdentifier`
Tasks that implement `TaskWithIdentifier` receive the generated ID before dispatch.
```go
type trackedTask struct {
ID string
Name string
}
func (t *trackedTask) SetTaskIdentifier(id string) { t.ID = id }
func (t *trackedTask) GetTaskIdentifier() string { return t.ID }
```
## Shutdown Interaction
When shutdown has started, `PerformAsync` returns an empty `Result` instead of scheduling more work.
This is why `ServiceShutdown` can safely drain the outstanding background tasks before stopping services.
When shutdown has started, `PerformAsync` returns an empty `Result`. `ServiceShutdown` drains outstanding background work before stopping services.

View file

@ -5,165 +5,172 @@ description: The repeated shapes that make CoreGO easy to navigate.
# Core Primitives
CoreGO is easiest to use when you read it as a small vocabulary repeated everywhere. Most of the framework is built from the same handful of types.
CoreGO is built from a small vocabulary repeated everywhere.
## Primitive Map
| Type | Used For |
|------|----------|
| `Options` | Input values and lightweight metadata |
| `Option` / `Options` | Input values and metadata |
| `Result` | Output values and success state |
| `Service` | Lifecycle-managed components |
| `Message` | Broadcast events |
| `Query` | Request-response lookups |
| `Task` | Side-effecting work items |
| `Action` | Named callable with panic recovery + entitlement |
| `Task` | Composed sequence of Actions |
| `Registry[T]` | Thread-safe named collection |
| `Entitlement` | Permission check result |
| `Message` | Broadcast events (ACTION) |
| `Query` | Request-response lookups (QUERY) |
## `Option` and `Options`
`Option` is one key-value pair. `Options` is an ordered slice of them.
```go
opts := core.Options{
{Key: "name", Value: "brain"},
{Key: "path", Value: "prompts"},
{Key: "debug", Value: true},
}
```
opts := core.NewOptions(
core.Option{Key: "name", Value: "brain"},
core.Option{Key: "path", Value: "prompts"},
core.Option{Key: "debug", Value: true},
)
Use the helpers to read values:
```go
name := opts.String("name")
path := opts.String("path")
debug := opts.Bool("debug")
hasPath := opts.Has("path")
raw := opts.Get("name")
raw := opts.Get("name") // Result{Value, OK}
opts.Has("path") // true
opts.Len() // 3
```
### Important Details
- `Get` returns the first matching key.
- `String`, `Int`, and `Bool` do not convert between types.
- Missing keys return zero values.
- CLI flags with values are stored as strings, so `--port=8080` should be read with `opts.String("port")`, not `opts.Int("port")`.
## `Result`
`Result` is the universal return shape.
Universal return shape. Every Core operation returns Result.
```go
r := core.Result{Value: "ready", OK: true}
type Result struct {
Value any
OK bool
}
r := c.Config().Get("host")
if r.OK {
fmt.Println(r.Value)
host := r.Value.(string)
}
```
It has two jobs:
- carry a value when work succeeds
- carry either an error or an empty state when work does not succeed
### `Result.Result(...)`
The `Result()` method adapts plain Go values and `(value, error)` pairs into a `core.Result`.
The `Result()` method adapts Go `(value, error)` pairs:
```go
r1 := core.Result{}.Result("hello")
r2 := core.Result{}.Result(file, err)
r := core.Result{}.Result(file, err)
```
This is how several built-in helpers bridge standard-library calls.
## `Service`
`Service` is the managed lifecycle DTO stored in the registry.
Managed lifecycle component stored in the `ServiceRegistry`.
```go
svc := core.Service{
Name: "cache",
Options: core.Options{
{Key: "backend", Value: "memory"},
},
OnStart: func() core.Result {
return core.Result{OK: true}
},
OnStop: func() core.Result {
return core.Result{OK: true}
},
OnReload: func() core.Result {
return core.Result{OK: true}
},
core.Service{
OnStart: func() core.Result { return core.Result{OK: true} },
OnStop: func() core.Result { return core.Result{OK: true} },
}
```
### Important Details
- `OnStart` and `OnStop` are used by the framework lifecycle.
- `OnReload` is stored on the service DTO, but CoreGO does not currently call it automatically.
- The registry stores `*core.Service`, not arbitrary typed service instances.
## `Message`, `Query`, and `Task`
These are simple aliases to `any`.
Or via `Startable`/`Stoppable` interfaces (preferred for named services):
```go
type Message any
type Query any
type Task any
type Startable interface { OnStartup(ctx context.Context) Result }
type Stoppable interface { OnShutdown(ctx context.Context) Result }
```
That means your own structs become the protocol:
## `Action`
Named callable — the atomic unit of work. Registered by name, invoked by name.
```go
type deployStarted struct {
Environment string
}
type ActionHandler func(context.Context, Options) Result
type workspaceCountQuery struct{}
type syncRepositoryTask struct {
Name string
type Action struct {
Name string
Handler ActionHandler
Description string
Schema Options
}
```
## `TaskWithIdentifier`
`Action.Run()` includes panic recovery and entitlement checking.
Long-running tasks can opt into task identifiers.
## `Task`
Composed sequence of Actions:
```go
type indexedTask struct {
ID string
type Task struct {
Name string
Description string
Steps []Step
}
func (t *indexedTask) SetTaskIdentifier(id string) { t.ID = id }
func (t *indexedTask) GetTaskIdentifier() string { return t.ID }
type Step struct {
Action string
With Options
Async bool
Input string // "previous" = output of last step
}
```
If a task implements `TaskWithIdentifier`, `PerformAsync` injects the generated `task-N` identifier before dispatch.
## `Registry[T]`
Thread-safe named collection with insertion order and 3 lock modes:
```go
r := core.NewRegistry[*MyService]()
r.Set("brain", svc)
r.Get("brain") // Result
r.Has("brain") // bool
r.Names() // []string (insertion order)
r.Each(func(name string, svc *MyService) { ... })
r.Lock() // fully frozen
r.Seal() // no new keys, updates OK
```
## `Entitlement`
Permission check result:
```go
type Entitlement struct {
Allowed bool
Unlimited bool
Limit int
Used int
Remaining int
Reason string
}
e := c.Entitled("social.accounts", 3)
e.NearLimit(0.8) // true if > 80% used
e.UsagePercent() // 75.0
```
## `Message` and `Query`
IPC type aliases for the anonymous broadcast system:
```go
type Message any // broadcast via ACTION
type Query any // request/response via QUERY
```
For typed, named dispatch use `c.Action("name").Run(ctx, opts)`.
## `ServiceRuntime[T]`
`ServiceRuntime[T]` is the small helper for packages that want to keep a Core reference and a typed options struct together.
Composition helper for services that need Core access and typed options:
```go
type agentServiceOptions struct {
WorkspacePath string
type MyService struct {
*core.ServiceRuntime[MyOptions]
}
type agentService struct {
*core.ServiceRuntime[agentServiceOptions]
}
runtime := core.NewServiceRuntime(c, agentServiceOptions{
WorkspacePath: "/srv/agent-workspaces",
})
runtime := core.NewServiceRuntime(c, MyOptions{BufferSize: 1024})
runtime.Core() // *Core
runtime.Options() // MyOptions
runtime.Config() // shortcut to Core().Config()
```
It exposes:
- `Core()`
- `Options()`
- `Config()`
This helper does not register anything by itself. It is a composition aid for package authors.

View file

@ -78,14 +78,12 @@ assert.Equal(t, "pong", c.QUERY("ping").Value)
```
```go
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
if t == "compute" {
return core.Result{Value: 42, OK: true}
}
return core.Result{}
c.Action("compute", func(_ context.Context, _ core.Options) core.Result {
return core.Result{Value: 42, OK: true}
})
assert.Equal(t, 42, c.PERFORM("compute").Value)
r := c.Action("compute").Run(context.Background(), core.NewOptions())
assert.Equal(t, 42, r.Value)
```
## Test Async Work