From dad3cd07381477cf75e4a27a26568a5ecd8b6d6d Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 7 Sep 2026 01:32:45 +0400 Subject: [PATCH] router: Router.Route consumes NormalizedInput directly (slice 11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change Route from (ctx, utterance string, now) to (ctx, input NormalizedInput, now). The ingress-constructed NormalizedInput now reaches the cascade intact — no reconstruction downstream. TryFastPath receives the same input, not a rebuilt one. All callers (production, eval framework, tests) updated to construct NormalizedInput. --- cmd/mavend/turnroute.go | 2 +- internal/router/eval/eval.go | 10 +++++----- internal/router/eval/llmrouter_test.go | 8 ++++---- internal/router/eval/reach.go | 2 +- internal/router/router.go | 6 ++++-- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/cmd/mavend/turnroute.go b/cmd/mavend/turnroute.go index 185b402..5cba3be 100644 --- a/cmd/mavend/turnroute.go +++ b/cmd/mavend/turnroute.go @@ -79,7 +79,7 @@ func (r *turnRoute) resolve(ctx context.Context) (router.Decision, bool, *dialog r.err = router.ErrNoIntents return } - r.dec, r.err = r.h.router.Route(ctx, r.input.Text, r.now) + r.dec, r.err = r.h.router.Route(ctx, r.input, r.now) }) return r.dec, r.cont, r.prev, r.err } diff --git a/internal/router/eval/eval.go b/internal/router/eval/eval.go index 59dc0cb..3d03fa4 100644 --- a/internal/router/eval/eval.go +++ b/internal/router/eval/eval.go @@ -103,17 +103,17 @@ func (f Fixture) Now() (time.Time, error) { // Router — the one thing a route decider must do to be scorable. *router.Router // satisfies it directly; an LLM-only path wraps its Route in RouterFunc. type Router interface { - Route(ctx context.Context, utterance string, now time.Time) (router.Decision, error) + Route(ctx context.Context, input router.NormalizedInput, now time.Time) (router.Decision, error) } // RouterFunc adapts a bare function to Router — for scoring a single stage // (e.g. *router.LLMRouter, whose Route returns an extra ok bool) without // standing up the whole cascade. -type RouterFunc func(ctx context.Context, utterance string, now time.Time) (router.Decision, error) +type RouterFunc func(ctx context.Context, input router.NormalizedInput, now time.Time) (router.Decision, error) // Route implements Router. -func (f RouterFunc) Route(ctx context.Context, utterance string, now time.Time) (router.Decision, error) { - return f(ctx, utterance, now) +func (f RouterFunc) Route(ctx context.Context, input router.NormalizedInput, now time.Time) (router.Decision, error) { + return f(ctx, input, now) } // Outcome — one scored case. Reasons is empty exactly when Pass is true. @@ -231,7 +231,7 @@ func Score(ctx context.Context, name string, r Router, f Fixture) (Report, error for _, c := range f.Cases { start := time.Now() - d, err := r.Route(ctx, c.Utterance, now) + d, err := r.Route(ctx, router.NormalizedInput{Text: c.Utterance}, now) o := Outcome{Case: c, Decision: d, Err: err, Latency: time.Since(start)} lat = append(lat, o.Latency) diff --git a/internal/router/eval/llmrouter_test.go b/internal/router/eval/llmrouter_test.go index d279856..d7037d1 100644 --- a/internal/router/eval/llmrouter_test.go +++ b/internal/router/eval/llmrouter_test.go @@ -74,8 +74,8 @@ func TestLLMRouterBaseline(t *testing.T) { // llm-only: the LLM stage in isolation. Route returns (Decision, ok, err); // !ok without an error would be a contract violation, so it is surfaced as // one rather than silently scored as a miss. - llmOnly := RouterFunc(func(ctx context.Context, u string, now time.Time) (router.Decision, error) { - d, ok, err := lr.Route(ctx, u, now) + llmOnly := RouterFunc(func(ctx context.Context, input router.NormalizedInput, now time.Time) (router.Decision, error) { + d, ok, err := lr.Route(ctx, input.Text, now) if err != nil { return d, err } @@ -152,8 +152,8 @@ func TestReachWithLLMRouter(t *testing.T) { lr := router.NewLLMRouter(client) m := router.DefaultActMatcher{Fns: actFns} - llmOnly := RouterFunc(func(ctx context.Context, u string, now time.Time) (router.Decision, error) { - d, ok, err := lr.Route(ctx, u, now) + llmOnly := RouterFunc(func(ctx context.Context, input router.NormalizedInput, now time.Time) (router.Decision, error) { + d, ok, err := lr.Route(ctx, input.Text, now) if err != nil { return d, err } diff --git a/internal/router/eval/reach.go b/internal/router/eval/reach.go index 8ad60a0..4e98142 100644 --- a/internal/router/eval/reach.go +++ b/internal/router/eval/reach.go @@ -214,7 +214,7 @@ func ScoreReach(ctx context.Context, name string, r Router, m router.ActMatcher, for _, c := range f.Cases { start := time.Now() - d, err := r.Route(ctx, c.Utterance, now) + d, err := r.Route(ctx, router.NormalizedInput{Text: c.Utterance}, now) o := ReachOutcome{Case: c, Decision: d, Err: err, Latency: time.Since(start)} lat = append(lat, o.Latency) diff --git a/internal/router/router.go b/internal/router/router.go index 70a3f77..da6f27e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -71,9 +71,11 @@ func New(cfg Config) *Router { // is flagged Clarify (the daemon asks rather than guesses — same shape as // since(key)==null → don't fire: a misrouted fact is a confident wrong write, // worse than a gap). -func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (Decision, error) { +func (r *Router) Route(ctx context.Context, input NormalizedInput, now time.Time) (Decision, error) { + utterance := input.Text + // stage 0 — deterministic fast path. First match wins; grammars are ordered. - fast, err := r.TryFastPath(ctx, NormalizedInput{Text: utterance}, now) + fast, err := r.TryFastPath(ctx, input, now) if err != nil { return Decision{}, err }