Documentation
¶
Overview ¶
Package scope is the Go side of the devtools' Scope (plan §13.3): the unit every discovery rung, deep link and API call carries — one conversation's public id and, optionally, the session, flow and run inside it — and the net/http middleware that hands it to the page.
A Scope has one string form, shared byte for byte with the web client's lib/scope.ts (the golden studio/testdata/scope.golden.json pins both):
pub_…;session=s_…;flow=f_…;run=r_…
the public id first and bare, the rest optional, in that fixed order, empty fields omitted, each value percent-encoded with encodeURIComponent's rules. Parse reads it back and never fails: a newer writer's field is not this reader's to reject.
The response header is the development rung: thread has no HTTP layer, so the app's own handler sets it, in one line —
mux.Handle("POST /chat", scope.Header(chat, func(r *http.Request) scope.Scope {
return scope.Scope{PublicID: publicIDOf(r)}
}))
and a handler that learns the run id only once its turn starts calls scope.Set(w, …) before it writes. The header never carries a token; a Scope has nowhere to put one.
Header also learns the page the request came from — its Referer, else its Origin — and stamps it, sanitised, as weft.page_url on every run the handler starts (core.WithMetadata on the request's context: no option for the handler to pass, thread's Send included), so Studio's "open in app" can return to that page with #weft_scope= and the devtools panel scopes itself there. PageURLFromContext reads it back.
The package lives beside the root rather than in it: the root package is a generated facade over core, which never imports net/http (ADR 0016, ADR 0027).
Index ¶
Examples ¶
Constants ¶
const HeaderName = "Weft-Scope"
HeaderName is the response header the app's handler sets and the panel reads.
const PageURLKey = "weft.page_url"
PageURLKey is the metadata key Header stamps the request's page URL under, on every run its handler starts (and so on each run's spans and records, and on Studio's run row).
Variables ¶
This section is empty.
Functions ¶
func Header ¶
Header sets the Weft-Scope response header to scopeOf(r) on every response next writes, unless that scope is zero, and exposes the header to cross-origin pages (Access-Control-Expose-Headers gains Weft-Scope; the app's CORS policy must still allow the page's origin). The header is set before next runs, so a streaming handler's first flush carries it; next may replace it with Set. A CORS layer inside Header that sets its own expose list replaces the entry, so the header is sent but a cross-origin page cannot read it: put scope.Header inside your CORS middleware, or list Weft-Scope in its exposed headers. The devtools panel reads a cross-origin response's header only when the page and the response are both on loopback (a dev server on :5173 calling the app on :8080); a cross-origin production app names its scope with data-scope or the DOM marker instead.
Header also puts the request's page URL (PageURL) on the request's context, for PageURLFromContext and — through core.WithMetadata — as weft.page_url on every run next starts under that context. A run's own core.Metadata option wins over it.
Example ¶
Header on the app's own chat endpoint, in one line: every response carries the conversation's scope, and the handler narrows it to the run once the turn has started. Cross-origin pages read the header through Access-Control-Expose-Headers, which Header sets; the app's CORS policy still has to allow the page's origin.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/weftgo/weft/scope"
)
func main() {
chat := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
runID := "r_01" // turn.RunID() once the turn is sent
scope.Set(w, scope.Scope{PublicID: r.URL.Query().Get("c"), RunID: runID})
_, _ = fmt.Fprintln(w, "hello")
})
h := scope.Header(chat, func(r *http.Request) scope.Scope {
return scope.Scope{PublicID: r.URL.Query().Get("c")}
})
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/chat?c=pub_demo", nil))
fmt.Println(w.Header().Get("Weft-Scope"))
fmt.Println(w.Header().Get("Access-Control-Expose-Headers"))
}
Output: pub_demo;run=r_01 Weft-Scope
func PageURL ¶ added in v0.14.0
PageURL is the page a request came from, sanitised: its Referer, else its Origin plus "/". Only an absolute http or https URL with a host counts; userinfo, the query and the fragment are dropped (a page, not a state: the panel re-adds #weft_scope=), and so is a URL over 1024 bytes, whole. "" when the request names no such page — no Referer or Origin, an opaque Origin ("null"), another scheme. The browser's default Referrer-Policy sends a cross-origin request only the origin, so a page on another origin is often its origin's root.
func PageURLFromContext ¶ added in v0.14.0
PageURLFromContext returns the page URL Header put on a request's context, and whether there was one.
Example ¶
Header stamps the page the request came from — its Referer, query and fragment dropped — as weft.page_url on every run the handler starts: the handler passes no option. Studio's run page reads it back for "open in app".
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"github.com/weftgo/weft/core"
"github.com/weftgo/weft/core/wefttest"
"github.com/weftgo/weft/scope"
)
func main() {
agt := core.New(wefttest.Script(wefttest.Say("hi")),
core.Tap(func(ctx context.Context, ev core.Event) {
if _, ok := ev.(core.RunStart); ok {
fmt.Println("run:", core.MetadataFromContext(ctx)[scope.PageURLKey])
}
}))
chat := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
page, _ := scope.PageURLFromContext(r.Context())
fmt.Println("handler:", page)
_, _ = agt.Generate(r.Context(), core.Prompt("hello"))
})
h := scope.Header(chat, func(*http.Request) scope.Scope { return scope.Scope{PublicID: "pub_demo"} })
req := httptest.NewRequest(http.MethodPost, "/chat", nil)
req.Header.Set("Referer", "https://app.example/support?draft=1#top")
h.ServeHTTP(httptest.NewRecorder(), req)
}
Output: handler: https://app.example/support run: https://app.example/support
func Set ¶
func Set(w http.ResponseWriter, s Scope)
Set sets the Weft-Scope header on w to s, replacing any earlier value, and exposes it to cross-origin pages (Weft-Scope is appended to Access-Control-Expose-Headers unless already listed; a "*" there does not cover it for a credentialed request). A zero s removes the Weft-Scope header but leaves the expose entry an earlier Set added. Like any header it must be set before the handler's first Write or WriteHeader — the dynamic case: a handler that knows the run id once its turn starts; a Set after that changes nothing sent.
Types ¶
type Scope ¶
type Scope struct {
PublicID string `json:"publicId"`
SessionID string `json:"session,omitempty"`
FlowID string `json:"flow,omitempty"`
RunID string `json:"run,omitempty"`
}
Scope names what the devtools follow. PublicID is the join key the app hands out ("" is the dev list: no conversation); the other fields narrow it. The JSON names are the web client's.
func Parse ¶
Parse reads a scope's string form back. The first ";"-segment is the public id; each later one is key=value. A key it does not know, a segment without "=", an empty value and a repeat of a key already read are ignored; a value that does not percent-decode is kept raw. Parse never fails.
Example ¶
package main
import (
"fmt"
"github.com/weftgo/weft/scope"
)
func main() {
s := scope.Parse("pub_demo;flow=f_3;tenant=acme;run=r_9")
fmt.Printf("%q %q %q %q\n", s.PublicID, s.SessionID, s.FlowID, s.RunID)
fmt.Println(s)
}
Output: "pub_demo" "" "f_3" "r_9" pub_demo;flow=f_3;run=r_9
func (Scope) String ¶
String is the scope's one string form: the public id first and bare, then session=, flow=, run= for each field set, joined by ";", every value percent-encoded. The zero Scope is "".
Example ¶
package main
import (
"fmt"
"github.com/weftgo/weft/scope"
)
func main() {
fmt.Println(scope.Scope{PublicID: "pub_demo", SessionID: "s_1", RunID: "r_2"})
fmt.Println(scope.Scope{PublicID: "a;b"})
}
Output: pub_demo;session=s_1;run=r_2 a%3Bb