core_execute Delegation
Package matrix/neo/internal/delegate is the bridge between Neo's conversational loop and the MCL pipeline. It hands a prose intent to the daemon's async HTTP API, services SSE-driven approval gates inline, and returns the verifiable outcome.
Package matrix/neo/internal/delegate is the bridge between Neo's conversational loop and the MCL pipeline. It hands a prose intent to the daemon's async HTTP API, services in-walk approval gates inline, and returns the verifiable outcome.
Source file: neo/internal/delegate/client.go.
Design decisions
SSE-first gate handling with poll fallback. Gate handling rides the daemon's SSE event stream (GET /events?intent_id=): gate.invoked triggers gate answering, intent.attest/fail/cancel are terminal. A poller is retained as an explicit safety net and fallback: it covers the dispatch-then-subscribe race and silent stream drops, and becomes the sole driver once bounded SSE reconnect attempts exhaust.
Inline approval. When the daemon fires a gate (e.g. "Approve spend of 5 PAX?"), the delegate blocks until the user answers via the conversation's gate-answer endpoint. The user never leaves the chat context.
Gate-claim deduplication. When multiple delegate clients attach to the same in-flight intent (a re-dispatch joining a parked launch), the GateClaim function serializes gate answering so two clients cannot double-answer the same gate.
Safe default. A nil Approver denies every gate; no unattended spends.
Bounded wait. MaxWait (default 30 minutes) prevents a stuck intent from blocking the conversation forever.
Client
type Client struct {
base string
http *http.Client
token string
did string
wallet string
skill string
approve Approver
notify func(string)
pollEvery time.Duration
maxWait time.Duration
}
delegate.New(delegate.Options{
BaseURL: "http://127.0.0.1:8080",
Token: os.Getenv("NEO_DAEMON_TOKEN"),
CallerDID: cfg.ActorDID,
CallerWallet: os.Getenv("NEO_CALLER_WALLET"),
Skill: "", // optional skill URI to pin
Approver: newApprover(in, rep),
Notify: rep.Notice,
GateClaim: func(intentID, nodeID string) bool { /* CAS claim */ },
})
Run
func (c *Client) Run(ctx context.Context, prose string) (string, error)
The full lifecycle:
- Submit -
POST /messages/asyncwith the prose intent, returnsintent_id - Subscribe -
GET /events?intent_id=for SSE-driven gate and status events - Gate loop - on
gate.invoked: ask approver,POST /intents/{id}/gates/{nid}/answer - Poll safety net - every
PollInterval(default 1.5s) in parallel:- Check for pending gates, answer them
- Check status, terminal states:
completed,failed,cancelled - Clarify questions, return as error ("needs more detail")
- Return - the deliverable answer, or an error describing the failure
Status handling
| Status | Outcome |
|---|---|
completed | Return Result.Answer (or "Done" if empty) |
failed | Return error with pipeline error message |
cancelled | Return error "the delegated task was cancelled" |
clarify present | Return error with the clarification question |
| timeout | Return error after maxWait |
Approver
type Approver func(ctx context.Context, nodeID, question string, options []string) (approved bool, answer string)
Called for every pending gate. The nodeID lets the UI route the user's answer back to the exact gate. Returns:
approved=true- the daemon proceeds with the spend/actionapproved=false- the daemon treats the gate as denied
CLI approver
In the interactive CLI, the approver reads from stdin:
approval needed, Approve spend of 5 PAX?
options: yes | no
approve? [y/N] y
Safe because Chat runs synchronously while the REPL is blocked; no concurrent stdin reads.
Server approver
In the HTTP service, the approver publishes a gate.invoked SSE event and blocks on a channel until the user answers via POST /intents/:id/gates/:nid/answer. The engine's gateClaims map ensures only one delegate client answers a given gate.
HTTP contract
Submit
POST /messages/async
{"prose": "send 5 PAX to 0x..."}
-> {"intent_id": "i1"}
Gates
GET /intents/{id}/gates
-> {"pending": [{"node_id": "n1", "question": "Approve spend?", "options": ["yes", "no"]}]}
POST /intents/{id}/gates/{nid}/answer
{"approved": true, "answer": ""}
Status
GET /messages/async/{id}
-> {"status": "completed", "result": {"answer": "settled"}}
Modifying the delegate
| What to change | Where |
|---|---|
| Poll interval | delegate/client.go - Options.PollInterval |
| Max wait | delegate/client.go - Options.MaxWait |
| HTTP timeout | delegate/client.go - Options.Timeout |
| Gate answer shape | delegate/client.go - answerGate() |
| Status response parsing | delegate/client.go - status() |
| Gate-claim dedup | delegate/client.go - Options.GateClaim |
| SSE reconnect budget | delegate/client.go - subscribeEvents() |
