> `dappco.re/go/core` — Dependency injection, service lifecycle, and message-passing framework.
> This document is the authoritative API contract. An agent should be able to write a service
> that registers with Core from this document alone.
**Status:** Living document
**Module:** `dappco.re/go/core`
**Version:** v0.7.0+
---
## 1. Core — The Container
Core is the central application container. Everything registers with Core, communicates through Core, and has its lifecycle managed by Core.
### 1.1 Creation
```go
c := core.New(
core.WithOption("name", "my-app"),
core.WithService(mypackage.Register),
core.WithService(anotherpackage.Register),
core.WithServiceLock(),
)
c.Run()
```
`core.New()` returns `*Core` (not Result — Core is the one type that can't wrap its own creation error). Functional options are applied in order. `WithServiceLock()` prevents late service registration.
### 1.2 Lifecycle
```
New() → WithService factories called → LockApply()
`Run()` is blocking. `ServiceStartup` calls `OnStartup(ctx)` on all services implementing `Startable`. `ServiceShutdown` calls `OnShutdown(ctx)` on all `Stoppable` services. Shutdown uses `context.Background()` — not the Core context (which is already cancelled).
Universal return type. Every Core operation returns Result.
```go
type Result struct {
Value any
OK bool
}
```
Usage patterns:
```go
// Check success
r := c.Config().Get("database.host")
if r.OK {
host := r.Value.(string)
}
// Service factory returns Result
func Register(c *core.Core) core.Result {
svc := &MyService{}
return core.Result{Value: svc, OK: true}
}
// Error as Result
return core.Result{Value: err, OK: false}
```
No generics on Result. Type-assert the Value when needed. This is deliberate — `Result` is universal across all subsystems without carrying type parameters.
### 2.4 Message, Query, Task
IPC type aliases — all are `any` at the type level, distinguished by usage:
```go
type Message any // broadcast via ACTION — fire and forget
type Query any // request/response via QUERY — returns first handler's result
type Task any // work unit via PERFORM — tracked with progress
```
---
## 3. Service System
### 3.1 Registration
Services register via factory functions passed to `WithService`:
```go
core.New(
core.WithService(mypackage.Register),
)
```
The factory signature is `func(*Core) Result`. The returned `Result.Value` is the service instance.
### 3.2 Factory Pattern
```go
func Register(c *core.Core) core.Result {
svc := &MyService{
runtime: core.NewServiceRuntime(c, MyOptions{}),
}
return core.Result{Value: svc, OK: true}
}
```
`NewServiceRuntime[T]` gives the service access to Core and typed options:
```go
type MyService struct {
*core.ServiceRuntime[MyOptions]
}
// Access Core from within the service:
func (s *MyService) doSomething() {
c := s.Core()
cfg := s.Config().String("my.setting")
}
```
### 3.3 Auto-Discovery
`WithService` reflects on the returned instance to discover:
- **Package name** → service name (from reflect type path)
- **Startable interface** → `OnStartup(ctx) error` called during `ServiceStartup`
- **Stoppable interface** → `OnShutdown(ctx) error` called during `ServiceShutdown`
- **HandleIPCEvents method** → auto-registered as IPC handler
### 3.4 Retrieval
```go
// Type-safe retrieval
svc, ok := core.ServiceFor[*MyService](c, "mypackage")
Core is infrastructure, not an encapsulated library. Downstream packages (core/agent, core/mcp, go-process) compose with Core's primitives. **Exported fields are intentional, not accidental.** Every unexported field that forces a consumer to write a wrapper method adds LOC downstream — the opposite of Core's purpose.
```go
// Core reduces downstream code:
if r.OK { use(r.Value) }
// vs Go convention that adds downstream LOC:
val, err := thing.Get()
if err != nil {
return fmt.Errorf("get: %w", err)
}
```
This is why `core.Result` exists — it replaces multiple lines of error handling with `if r.OK {}`. That's the design: expose the primitive, reduce consumer code.
### Export Rules
| Should Export | Why |
|--------------|-----|
| Struct fields used by consumers | Removes accessor boilerplate downstream |
| Mutexes and sync primitives | Concurrency must be managed by Core |
| Context/cancel pairs | Lifecycle is Core's responsibility |
| Internal counters | Implementation detail, not a brick |
### Why core/go Is Minimal
core/go deliberately avoids importing anything beyond stdlib + go-io + go-log. This keeps it as a near-pure stdlib implementation. Packages that add external dependencies (CLI frameworks, HTTP routers, MCP SDK) live in separate repos:
```
core/go — pure primitives (stdlib only)
core/go-process — process management (adds os/exec)
Each layer imports the one below. core/go imports nothing from the ecosystem — everything imports core/go.
## Known Issues
### 1. Dual IPC Naming
`ACTION()` and `Action()` do the same thing. `QUERY()` and `Query()`. Two names for one operation. Pick one or document when to use which.
```go
// Currently both exist:
c.ACTION(msg) // uppercase alias
c.Action(msg) // actual implementation
```
**Recommendation:** Keep both — `ACTION`/`QUERY`/`PERFORM` are the public "intent" API (semantically loud, used by services). `Action`/`Query`/`Perform` are the implementation methods. Document: services use uppercase, Core internals use lowercase.
### 2. MustServiceFor Uses Panic
```go
func MustServiceFor[T any](c *Core, name string) T {
panic(...)
}
```
RFC-025 says "no hidden panics." `Must` prefix signals it, but the pattern contradicts the Result philosophy. Consider deprecating in favour of `ServiceFor` + `if !ok` pattern.
### 3. Embed() Legacy Accessor
```go
func (c *Core) Embed() Result { return c.data.Get("app") }
```
Dead accessor with "use Data()" comment. Should be removed — it's API surface clutter that confuses agents.
### 4. Package-Level vs Core-Level Logging
```go
core.Info("msg") // global default logger
c.Log().Info("msg") // Core's logger instance
```
Both work. Global functions exist for code without Core access (early init, proc.go helpers). Services with Core access should use `c.Log()`. Document the boundary.
### 5. RegisterAction Lives in task.go
IPC registration (`RegisterAction`, `RegisterActions`, `RegisterTask`) is in `task.go` but the dispatch functions (`Action`, `Query`, `QueryAll`) are in `ipc.go`. All IPC should be in one file or the split should follow a clear boundary (dispatch vs registration).
### 6. serviceRegistry Is Unexported
`serviceRegistry` is unexported, meaning consumers can't extend service management. Per the Lego Bricks philosophy, this should be exported so downstream packages can build on it.
### 7. No c.Process() Accessor
Process management (go-process) should be a Core subsystem accessor like `c.Fs()`, not a standalone service retrieved via `ServiceFor`. Planned for go-process v0.7.0 update.
### 8. NewRuntime / NewWithFactories — Legacy
These pre-v0.7.0 functions take `app any` instead of `*Core`. Verify if they're still used — if not, deprecate.