From 49584e8b4c508cd2476ab84a9ffb069916255dcb Mon Sep 17 00:00:00 2001 From: "user.email" Date: Wed, 25 Mar 2026 17:41:08 +0000 Subject: [PATCH] =?UTF-8?q?fix(rfc-025):=20IPC=20example=20=E2=86=92=20nam?= =?UTF-8?q?ed=20Actions,=20JSON=20primitive=20now=20exists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replaced anonymous broadcast example with named Action pattern - Added Task composition example - Moved ACTION/QUERY to "Legacy Layer" subsection - Principle 9: encoding/json now has core.JSONMarshal() replacement Co-Authored-By: Virgil --- docs/specs/RFC-025-AGENT-EXPERIENCE.md | 54 ++++++++++++++++++++------ 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/docs/specs/RFC-025-AGENT-EXPERIENCE.md b/docs/specs/RFC-025-AGENT-EXPERIENCE.md index ee61516..f4eee97 100644 --- a/docs/specs/RFC-025-AGENT-EXPERIENCE.md +++ b/docs/specs/RFC-025-AGENT-EXPERIENCE.md @@ -215,25 +215,55 @@ opts.Int("port") // 8080 core.Result{Value: svc, OK: true} ``` -#### IPC — Event-Driven Communication +#### Named Actions — The Primary Communication Pattern -Services communicate via typed messages through Core's ACTION system. No direct function calls between services — declare intent, let the event system route it. +Services register capabilities as named Actions. No direct function calls, no untyped dispatch — declare intent by name, invoke by name. ```go -// Broadcast an event -c.ACTION(messages.AgentCompleted{ - Agent: "codex", Repo: "go-io", Status: "completed", +// Register a capability during OnStartup +c.Action("workspace.create", func(ctx context.Context, opts core.Options) core.Result { + name := opts.String("name") + path := core.JoinPath("/srv/workspaces", name) + return core.Result{Value: path, OK: true} }) -// Register a handler -c.RegisterAction(func(c *core.Core, msg core.Message) core.Result { - if ev, ok := msg.(messages.AgentCompleted); ok { - // react to completion - } - return core.Result{OK: true} +// Invoke by name — typed, inspectable, entitlement-checked +r := c.Action("workspace.create").Run(ctx, core.NewOptions( + core.Option{Key: "name", Value: "alpha"}, +)) + +// Check capability before calling +if c.Action("process.run").Exists() { /* go-process is registered */ } + +// List all capabilities +c.Actions() // ["workspace.create", "process.run", "brain.recall", ...] +``` + +#### Task Composition — Sequencing Actions + +```go +c.Task("agent.completion", core.Task{ + Steps: []core.Step{ + {Action: "agentic.qa"}, + {Action: "agentic.auto-pr"}, + {Action: "agentic.verify"}, + {Action: "agentic.poke", Async: true}, // doesn't block + }, }) ``` +#### Anonymous Broadcast — Legacy Layer + +`ACTION` and `QUERY` remain for backwards-compatible anonymous dispatch. New code should prefer named Actions. + +```go +// Broadcast — all handlers fire, type-switch to filter +c.ACTION(messages.DeployCompleted{Env: "production"}) + +// Query — first responder wins +r := c.QUERY(countQuery{}) +``` + #### Process Execution — Use Core Primitives All external command execution MUST go through `c.Process()`, not raw `os/exec`. This makes process execution testable, gatable by entitlements, and managed by Core's lifecycle. @@ -326,7 +356,7 @@ Core primitives become mechanical code review rules. An agent reviewing a diff c |--------|-----------|-------------| | `os/exec` | Bypasses Process primitive | `c.Process().Run()` | | `unsafe` | Bypasses Fs sandbox | `Fs.NewUnrestricted()` | -| `encoding/json` | Bypasses Core serialisation | Core JSON helpers (future) | +| `encoding/json` | Bypasses Core serialisation | `core.JSONMarshal()` / `core.JSONUnmarshal()` | | `fmt.Errorf` | Bypasses error primitive | `core.E()` | | `errors.New` | Bypasses error primitive | `core.E()` | | `log.*` | Bypasses logging | `core.Info()` / `c.Log()` |