Matrix logo

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:

  1. Submit - POST /messages/async with the prose intent, returns intent_id
  2. Subscribe - GET /events?intent_id= for SSE-driven gate and status events
  3. Gate loop - on gate.invoked: ask approver, POST /intents/{id}/gates/{nid}/answer
  4. 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")
  5. Return - the deliverable answer, or an error describing the failure

Status handling

StatusOutcome
completedReturn Result.Answer (or "Done" if empty)
failedReturn error with pipeline error message
cancelledReturn error "the delegated task was cancelled"
clarify presentReturn error with the clarification question
timeoutReturn 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/action
  • approved=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 changeWhere
Poll intervaldelegate/client.go - Options.PollInterval
Max waitdelegate/client.go - Options.MaxWait
HTTP timeoutdelegate/client.go - Options.Timeout
Gate answer shapedelegate/client.go - answerGate()
Status response parsingdelegate/client.go - status()
Gate-claim dedupdelegate/client.go - Options.GateClaim
SSE reconnect budgetdelegate/client.go - subscribeEvents()