context: magpie inspects the context window of every request, session and agent
The gateway reads each request's prompt into its parts (system prompt, tools and MCP servers, memory, files, tool results, chat), measured against the model's window and calibrated to the tokens the vendor counted. Routing shows a request's window as a card: a 400-cell grid by part, a hover naming what is largest in a cell, and the contents by part. The Usage page's Context tab scores each agent out of 100 (cache reuse, how lean it starts, how fast it grows, how reliably it answers), tags it (lean, cache-friendly, mcp-heavy, compacts, near-full…), and draws each session's growth with its latest request's card. Older Safari (15) gets media-query and solid-colour fallbacks for the card's container queries and color-mix.
This commit is contained in:
1 parent
569ca6f005
commit
586f2bce8f
24 files changed
+5133
-7
No files matched your search
@@ -24,6 +24,7 @@ served by a plugin are in [Provider and plugin ownership](provider-plugins.md).
|
||||
| Affinity | A conversation stays with the key or account that answered it, so the vendor's prompt cache is read again. Modes: auto (""), `session`, `turn`, `off`. Stored in `affinity.json` next to `providers.json`. | [`affinity.go`](../../internal/gateway/affinity.go) |
|
||||
| One try | `attempt` sends one candidate the request. It goes through the provider's proxy (`p.Via`). A subscription with its own client is served specially, such as Claude through the genuine Claude Code. The request is relayed as is (`passthrough`) when the model is usable on the agent's API, except that a Claude model where the provider has an Anthropic URL is translated to Messages from Chat or Responses too (`OnMessages`: OpenCode speaks Chat for every model, and a relay drops cache_control there), unless the user set the model's API or the vendor's list names its APIs. A request offering web search to a provider that doesn't search by itself is translated too, so magpie can search for it (`search.go`), but on the agent's own API where the provider serves it and no `ResponsesFirst`/`MessagesFirst` rule prefers another: Codex offers web_search on every turn, and each went out as Chat (#997). Otherwise it is translated (`translate`) to the first API `usable` gives: Chat, except Responses for OpenAI's models where their maker serves them (`ResponsesFirst`) and Anthropic's Messages for a Claude model where the provider has an Anthropic URL (`MessagesFirst`, which keeps its cache breakpoints, #997). `Provider.Native`, the API agents with a per-model API (Pi, omp, droid) are wired to ask on, follows the same order. | [`gateway.go`](../../internal/gateway/gateway.go) |
|
||||
| Trace | The Routing view's record of each decision, written where the decision is made: candidates, rests, which try answered. | [`trace.go`](../../internal/gateway/trace.go) |
|
||||
| Prompt | What each request's prompt held, part by part: the agent's system prompt, tool definitions (an MCP server's tools as one item), instruction files and skills, files its tools read, their other output, and the conversation. `inspectPrompt` reads it from the body as the agent sent it, before translation; `calibrated` scales the estimate to the prompt the vendor counted and sets the window of the model that answered. Only names are kept, never the text. It goes on the trace as `Route.Prompt`, with `Route.Conv` naming a conversation that has no session id. The GUI's `/api/context` scores each agent from it (`internal/gui/context.go`). | [`prompt.go`](../../internal/gateway/prompt.go) |
|
||||
|
||||
## Runtime path
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ three.
|
||||
| Backend | `startBackend` starts the gateway this process serves, unless another magpie already serves it. In that case this process only shows the other one's status (`gw` is nil). `stopServing` drains requests in flight. | [`backend.go`](../../internal/gui/backend.go) |
|
||||
| API and assets | `Handler` serves `assets/` (embedded with `//go:embed assets`) and the `/api/*` routes, such as `/api/state`, `/api/set` and `/api/agents/{action}/{id}`. `held` lets one API request's reads share one catalog build (`provider.Hold`), and drops it after any write. `revalidated` and `versionedPage` name each script by its content hash, so a cache never serves an old `app.js` under a new page. | [`api.go`](../../internal/gui/api.go) |
|
||||
| Browser mode | `StartWeb` serves the page and the gateway for `magpie web`. The browser signs in with a run key in a link. Gateway mode shows only Providers, Gateway, Routing, Usage, Plugins and Settings on a machine that is only a gateway. | [`web.go`](../../internal/gui/web.go), [`gatewaymode.go`](../../internal/gui/gatewaymode.go) |
|
||||
| The page | `app.js` keeps one state object per view and renders it into a list, with no framework. The views have their own files: `routing.js`, `library.js`, `plugins.js` and `sessions.js`. Strings go through `t()` with translations in `i18n.js` (zh, zh-TW, ja and de; the English text is the key and the fallback). zh-TW is Traditional Chinese with Taiwan's words; a system language of zh-TW, zh-HK, zh-MO or any zh-Hant picks it, and the tray menu and notifications follow (`trayLang`). Dropdowns use `openProtoMenu`. Scrolling is done only by `scrollOnPurpose(e)` with the reader's event. | [`assets/app.js`](../../internal/gui/assets/app.js), [`assets/i18n.js`](../../internal/gui/assets/i18n.js) |
|
||||
| The page | `app.js` keeps one state object per view and renders it into a list, with no framework. The views have their own files: `routing.js`, `library.js`, `plugins.js`, `sessions.js` and `context.js` (Usage's Context tab, and the context window card the Routing page draws under a request's story, fed by `/api/context` in `context.go`, which scores and tags each agent). Strings go through `t()` with translations in `i18n.js` (zh, zh-TW, ja and de; the English text is the key and the fallback). zh-TW is Traditional Chinese with Taiwan's words; a system language of zh-TW, zh-HK, zh-MO or any zh-Hant picks it, and the tray menu and notifications follow (`trayLang`). Dropdowns use `openProtoMenu`. Scrolling is done only by `scrollOnPurpose(e)` with the reader's event. | [`assets/app.js`](../../internal/gui/assets/app.js), [`assets/i18n.js`](../../internal/gui/assets/i18n.js) |
|
||||
| No GUI build | `-tags nogui` builds magpie without the GUI: `hasGUI` is false and `runGUI` says to use `magpie tui`. The files of `internal/gui` that need Wails build under `!nogui`. | [`gui_off.go`](../../gui_off.go) |
|
||||
|
||||
## Runtime path
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/yetone/magpie/internal/access"
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/codexcat"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/usage"
|
||||
@@ -429,8 +430,9 @@ func (s *Server) codexUpstream(w http.ResponseWriter, r *http.Request, rest stri
|
||||
seat := Weighed{ID: "codex", Provider: "openai", Name: "OpenAI", Icon: "openai", Who: who, Kind: "account", Agent: "codex", Model: model}
|
||||
link := s.titlePrompts.observe(r, body, metadata, kind, start)
|
||||
captureTitle = link != nil && isTitleKind(kind)
|
||||
tr = s.trace.begin(Route{imageTurn: drawingTurnID(metadata.Turn), imageCaller: codexTurnKey(r, callerOf(r).agent), imageProvider: imageProvider, TitleLink: link, Time: start, Agent: agentOf(r), Session: sessionOf(r.Header), ParentSession: titleParentSession(r.Header, metadata, kind), Kind: kind, Model: model, Effort: effort, Provider: "openai",
|
||||
tr = s.trace.begin(Route{imageTurn: drawingTurnID(metadata.Turn), imageCaller: codexTurnKey(r, callerOf(r).agent), imageProvider: imageProvider, TitleLink: link, Time: start, Agent: agentOf(r), Session: sessionOf(r.Header), Conv: convOf(r.Header, body), ParentSession: titleParentSession(r.Header, metadata, kind), Kind: kind, Model: model, Effort: effort, Provider: "openai",
|
||||
Order: []Weighed{seat}, Tries: []Try{{ID: seat.ID, Model: model, Effort: effort, Start: start}}})
|
||||
promptRead := s.inspectPrompt(tr, provider.Responses, body)
|
||||
end = func(status int, msg string, tokens, out int) {
|
||||
ms := time.Since(start).Milliseconds()
|
||||
ttft, text := first.ms()
|
||||
@@ -438,6 +440,7 @@ func (s *Server) codexUpstream(w http.ResponseWriter, r *http.Request, rest stri
|
||||
if captureTitle && status < 400 && msg == "" && !titleReply.truncated {
|
||||
replyDigest = titleReplyDigest(titleReply.buf.Bytes(), nil)
|
||||
}
|
||||
prompt := promptRead() // outside the trace's lock, which reading it takes
|
||||
s.trace.update(tr, func(t *Route) {
|
||||
t.Tries[0].Done, t.Tries[0].Status, t.Tries[0].Millis, t.Tries[0].Error = true, status, ms, msg
|
||||
t.Tries[0].TTFT, t.Tries[0].FirstText = ttft, text
|
||||
@@ -449,6 +452,9 @@ func (s *Server) codexUpstream(w http.ResponseWriter, r *http.Request, rest stri
|
||||
}
|
||||
t.Output, t.Reasoning, t.TTFT, t.FirstText = out, uu.Reasoning, ttft, text
|
||||
t.Usage = routeUsage("openai", model, uu)
|
||||
if prompt != nil {
|
||||
t.Prompt = prompt.calibrated(promptCounted(t.Usage), catalog.ContextOf(model))
|
||||
}
|
||||
t.Tries[0].Served, t.Tries[0].Swapped = served, swapped(model, served)
|
||||
t.Served, t.Swapped = t.Tries[0].Served, t.Tries[0].Swapped
|
||||
})
|
||||
|
||||
@@ -1634,10 +1634,12 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request, from provider.Pro
|
||||
shown = nil // nobody else to stay away from
|
||||
}
|
||||
link := s.titlePrompts.observe(r, body, metadata, call.Kind, start)
|
||||
tr := s.trace.begin(Route{imageTurn: drawingTurnID(metadata.Turn), imageCaller: codexTurnKey(r, call.Agent), TitleLink: link, Pinned: pin, Time: start, Agent: call.Agent, Session: sessionOf(r.Header), ParentSession: titleParentSession(r.Header, metadata, call.Kind), Kind: call.Kind, For: call.For, Model: call.Model, Effort: requestEffort(from, body), Provider: p.ID, Group: group, Rule: hit, Nested: nested, SealedTask: sealedTask, LeadAccount: leadAccount, Affinity: shown, Order: pl.order, Left: pl.left})
|
||||
tr := s.trace.begin(Route{imageTurn: drawingTurnID(metadata.Turn), imageCaller: codexTurnKey(r, call.Agent), TitleLink: link, Pinned: pin, Time: start, Agent: call.Agent, Session: sessionOf(r.Header), Conv: convOf(r.Header, body), ParentSession: titleParentSession(r.Header, metadata, call.Kind), Kind: call.Kind, For: call.For, Model: call.Model, Effort: requestEffort(from, body), Provider: p.ID, Group: group, Rule: hit, Nested: nested, SealedTask: sealedTask, LeadAccount: leadAccount, Affinity: shown, Order: pl.order, Left: pl.left})
|
||||
if telemetry != nil {
|
||||
telemetry.routeID = tr.ID
|
||||
}
|
||||
promptRead := s.inspectPrompt(tr, from, body)
|
||||
var lastTried provider.Provider // the last try's, for its model's context window
|
||||
var skipped []string
|
||||
sent := "" // the reasoning the last try's model was asked for
|
||||
where := "" // the last try's provider.Where, for the usage
|
||||
@@ -1690,6 +1692,7 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request, from provider.Pro
|
||||
}
|
||||
call.Provider, call.To, call.Usage = c.p.ID, "", Usage{}
|
||||
model = c.model
|
||||
lastTried = c.p
|
||||
where = c.p.Where()
|
||||
providerKeyID, providerKeyName = "", ""
|
||||
if c.p.Account == nil && c.p.Key != "" {
|
||||
@@ -2343,6 +2346,10 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request, from provider.Pro
|
||||
if link != nil && isTitleKind(call.Kind) && call.Status < 400 && call.Error == "" && !call.ResponseTruncated {
|
||||
replyDigest = titleReplyDigest([]byte(call.ResponseBody), replyTitleShape(r.Context()))
|
||||
}
|
||||
prompt, window := promptRead(), 0
|
||||
if prompt != nil && lastTried.ID != "" {
|
||||
window = windowOf(lastTried, model)
|
||||
}
|
||||
s.trace.update(tr, func(t *Route) {
|
||||
t.Done, t.Status, t.Error, t.Millis = true, call.Status, call.Error, call.Millis
|
||||
if t.TitleLink != nil && isTitleKind(t.Kind) && call.Status < 400 && call.Error == "" && !call.ResponseTruncated {
|
||||
@@ -2355,6 +2362,9 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request, from provider.Pro
|
||||
}
|
||||
t.Tokens = call.Usage.Input + call.Usage.Output + call.Usage.CacheRead + call.Usage.CacheWrite
|
||||
t.Output, t.Reasoning, t.TTFT, t.FirstText = call.Usage.Output, call.Usage.Reasoning, call.TTFT, call.FirstText
|
||||
if prompt != nil {
|
||||
t.Prompt = prompt.calibrated(promptCounted(t.Usage), window)
|
||||
}
|
||||
if n := len(t.Tries); n > 0 && call.Status < 400 {
|
||||
t.Served, t.Swapped, t.Routed = t.Tries[n-1].Served, t.Tries[n-1].Swapped, t.Tries[n-1].Routed
|
||||
t.Upstream = t.Tries[n-1].Upstream
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,286 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
)
|
||||
|
||||
func fixturePrompt(t *testing.T, name string, from provider.Protocol) *Prompt {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile("testdata/context/" + name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := promptOf(from, b)
|
||||
if p == nil {
|
||||
t.Fatalf("%s: no prompt", name)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func partOf(p *Prompt, kind string) PromptPart {
|
||||
for _, x := range p.Parts {
|
||||
if x.Kind == kind {
|
||||
return x
|
||||
}
|
||||
}
|
||||
return PromptPart{}
|
||||
}
|
||||
|
||||
func itemOf(p *Prompt, kind, name string) (PromptItem, bool) {
|
||||
for _, it := range partOf(p, kind).Items {
|
||||
if it.Name == name {
|
||||
return it, true
|
||||
}
|
||||
}
|
||||
return PromptItem{}, false
|
||||
}
|
||||
|
||||
// A Claude Code request, as Claude Code 2.1 sends it (testdata, its text
|
||||
// replaced with filler of the same length): the CLAUDE.md files in its
|
||||
// first system-reminder are memory, whose they are told by how Claude Code
|
||||
// describes them; an MCP server's tools are one item; the files Read read
|
||||
// are files, by path; ls's output is Bash's; the harness's mid-conversation
|
||||
// system messages are its own prompt.
|
||||
func TestPromptOfClaudeCode(t *testing.T) {
|
||||
p := fixturePrompt(t, "claude-code.json", provider.Anthropic)
|
||||
var kinds []string
|
||||
for _, x := range p.Parts {
|
||||
kinds = append(kinds, x.Kind)
|
||||
}
|
||||
if !slices.Equal(kinds, []string{PartSystem, PartTools, PartMemory, PartFiles, PartResults, PartChat}) {
|
||||
t.Fatalf("parts %v", kinds)
|
||||
}
|
||||
if it, ok := itemOf(p, PartMemory, "/home/dev/.claude/CLAUDE.md"); !ok || it.Tag != "user" {
|
||||
t.Fatalf("user CLAUDE.md %+v", partOf(p, PartMemory))
|
||||
}
|
||||
if it, ok := itemOf(p, PartMemory, "/home/dev/proj/CLAUDE.md"); !ok || it.Tag != "project" {
|
||||
t.Fatalf("project CLAUDE.md %+v", partOf(p, PartMemory))
|
||||
}
|
||||
if _, ok := itemOf(p, PartMemory, "reminders"); !ok {
|
||||
t.Fatalf("the rest of the reminders %+v", partOf(p, PartMemory))
|
||||
}
|
||||
if it, ok := itemOf(p, PartTools, "linear"); !ok || it.Tag != "mcp" || it.N != 3 {
|
||||
t.Fatalf("linear's tools %+v", it)
|
||||
}
|
||||
if _, ok := itemOf(p, PartTools, "Bash"); !ok {
|
||||
t.Fatal("Bash's definition")
|
||||
}
|
||||
for _, f := range []string{"/home/dev/proj/main.go", "/home/dev/proj/reconcile.go"} {
|
||||
if it, ok := itemOf(p, PartFiles, f); !ok || it.Tag != "read" {
|
||||
t.Fatalf("%s: %+v", f, partOf(p, PartFiles))
|
||||
}
|
||||
}
|
||||
if it, ok := itemOf(p, PartResults, "Bash"); !ok || it.Tag != "result" {
|
||||
t.Fatalf("ls %+v", partOf(p, PartResults))
|
||||
}
|
||||
if _, ok := itemOf(p, PartSystem, "environment"); !ok {
|
||||
t.Fatalf("the # Environment system message %+v", partOf(p, PartSystem))
|
||||
}
|
||||
if p.Turns != 1 || p.Counted || p.Held {
|
||||
t.Fatalf("%+v", p)
|
||||
}
|
||||
sum := 0
|
||||
for _, x := range p.Parts {
|
||||
sum += x.Tokens
|
||||
}
|
||||
if sum != p.Tokens || p.Tokens < 10000 {
|
||||
t.Fatalf("tokens %d, parts %d", p.Tokens, sum)
|
||||
}
|
||||
}
|
||||
|
||||
// A Codex request, as Codex 0.1xx sends ChatGPT's backend: its tools come
|
||||
// in additional_tools, its own functions one by one and any other
|
||||
// namespace as one; AGENTS.md comes as a user message; the skills list as
|
||||
// a developer one; and exec's JavaScript reading files with cat reads them.
|
||||
func TestPromptOfCodex(t *testing.T) {
|
||||
p := fixturePrompt(t, "codex.json", provider.Responses)
|
||||
if it, ok := itemOf(p, PartMemory, "/home/dev/proj/AGENTS.md"); !ok || it.Tag != "project" {
|
||||
t.Fatalf("AGENTS.md %+v", partOf(p, PartMemory))
|
||||
}
|
||||
if it, ok := itemOf(p, PartMemory, "skills"); !ok || it.Tag != "skills" {
|
||||
t.Fatalf("skills %+v", partOf(p, PartMemory))
|
||||
}
|
||||
if it, ok := itemOf(p, PartTools, "collaboration"); !ok || it.Tag != "namespace" || it.N < 2 {
|
||||
t.Fatalf("collaboration %+v", partOf(p, PartTools))
|
||||
}
|
||||
if _, ok := itemOf(p, PartTools, "exec"); !ok {
|
||||
t.Fatalf("exec %+v", partOf(p, PartTools))
|
||||
}
|
||||
for _, f := range []string{"main.go", "reconcile.go"} {
|
||||
if it, ok := itemOf(p, PartFiles, f); !ok || it.Tag != "shell" {
|
||||
t.Fatalf("%s: %+v", f, partOf(p, PartFiles))
|
||||
}
|
||||
}
|
||||
if _, ok := itemOf(p, PartSystem, "environment_context"); !ok {
|
||||
t.Fatalf("environment %+v", partOf(p, PartSystem))
|
||||
}
|
||||
if _, ok := itemOf(p, PartSystem, "prompt"); !ok {
|
||||
t.Fatalf("Codex's own prompt %+v", partOf(p, PartSystem))
|
||||
}
|
||||
if p.Turns != 1 {
|
||||
t.Fatalf("turns %d", p.Turns)
|
||||
}
|
||||
}
|
||||
|
||||
// Codex sends an MCP server's tools as a namespace named as its tools are,
|
||||
// mcp__<server>__: that one is the server's, its own (clock…) aren't MCP.
|
||||
func TestPromptOfCodexMCP(t *testing.T) {
|
||||
body := `{"model":"gpt-5.5","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}],"tools":[
|
||||
{"type":"namespace","name":"mcp__linear__","tools":[{"type":"function","name":"list_issues","description":"` + strings.Repeat("issues ", 200) + `"},{"type":"function","name":"get_issue","description":"one issue"}]},
|
||||
{"type":"namespace","name":"clock","tools":[{"type":"function","name":"sleep","description":"sleeps"}]}]}`
|
||||
p := promptOf(provider.Responses, []byte(body))
|
||||
if it, ok := itemOf(p, PartTools, "linear"); !ok || it.Tag != "mcp" || it.N != 2 {
|
||||
t.Fatalf("linear %+v", partOf(p, PartTools))
|
||||
}
|
||||
if it, ok := itemOf(p, PartTools, "clock"); !ok || it.Tag != "namespace" {
|
||||
t.Fatalf("clock %+v", partOf(p, PartTools))
|
||||
}
|
||||
}
|
||||
|
||||
// Chat Completions: a tool's answer goes by the name of the call it
|
||||
// answers, a file it read by the file; a user's message begins a turn.
|
||||
func TestPromptOfChat(t *testing.T) {
|
||||
body := `{"model":"m","messages":[
|
||||
{"role":"system","content":"You are OpenCode.\nInstructions from: /p/AGENTS.md\nbe brief, always"},
|
||||
{"role":"user","content":"look at a.go"},
|
||||
{"role":"assistant","content":null,"tool_calls":[{"id":"c1","type":"function","function":{"name":"read","arguments":"{\"filePath\":\"/p/a.go\"}"}},
|
||||
{"id":"c2","type":"function","function":{"name":"bash","arguments":"{\"command\":\"go test ./...\"}"}}]},
|
||||
{"role":"tool","tool_call_id":"c1","content":"package a\nfunc A() {}"},
|
||||
{"role":"tool","tool_call_id":"c2","content":"ok a 0.1s"},
|
||||
{"role":"user","content":[{"type":"text","text":"and this"},{"type":"image_url","image_url":{"url":"data:image/png;base64,AAAA"}}]}],
|
||||
"tools":[{"type":"function","function":{"name":"read","parameters":{}}},{"type":"function","function":{"name":"mcp__gh__issue","parameters":{}}}]}`
|
||||
p := promptOf(provider.Chat, []byte(body))
|
||||
if p == nil || p.Turns != 2 {
|
||||
t.Fatalf("%+v", p)
|
||||
}
|
||||
if it, ok := itemOf(p, PartMemory, "/p/AGENTS.md"); !ok || it.Tag != "project" {
|
||||
t.Fatalf("OpenCode's instructions %+v", partOf(p, PartMemory))
|
||||
}
|
||||
if _, ok := itemOf(p, PartFiles, "/p/a.go"); !ok {
|
||||
t.Fatalf("files %+v", partOf(p, PartFiles))
|
||||
}
|
||||
if it, ok := itemOf(p, PartFiles, "image"); !ok || it.Tokens != imageTokens {
|
||||
t.Fatalf("image %+v", it)
|
||||
}
|
||||
if _, ok := itemOf(p, PartResults, "bash"); !ok {
|
||||
t.Fatalf("results %+v", partOf(p, PartResults))
|
||||
}
|
||||
if it, ok := itemOf(p, PartTools, "gh"); !ok || it.Tag != "mcp" {
|
||||
t.Fatalf("tools %+v", partOf(p, PartTools))
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini matches a function's response to its call by name.
|
||||
func TestPromptOfGemini(t *testing.T) {
|
||||
body := `{"systemInstruction":{"parts":[{"text":"You are Gemini CLI."}]},
|
||||
"contents":[{"role":"user","parts":[{"text":"read go.mod"}]},
|
||||
{"role":"model","parts":[{"functionCall":{"name":"read_file","args":{"path":"go.mod"}}}]},
|
||||
{"role":"user","parts":[{"functionResponse":{"name":"read_file","response":{"output":"module x"}}}]}],
|
||||
"tools":[{"functionDeclarations":[{"name":"read_file","parameters":{}}]}]}`
|
||||
p := promptOf(provider.Gemini, []byte(body))
|
||||
if p == nil || p.Turns != 1 {
|
||||
t.Fatalf("%+v", p)
|
||||
}
|
||||
if _, ok := itemOf(p, PartFiles, "go.mod"); !ok {
|
||||
t.Fatalf("files %+v", p.Parts)
|
||||
}
|
||||
}
|
||||
|
||||
// A Responses request that names a previous response holds its earlier
|
||||
// turns at the vendor's.
|
||||
func TestPromptHeld(t *testing.T) {
|
||||
p := promptOf(provider.Responses, []byte(`{"previous_response_id":"resp_1","input":[{"role":"user","content":"go on"}]}`))
|
||||
if p == nil || !p.Held || p.Turns != 1 {
|
||||
t.Fatalf("%+v", p)
|
||||
}
|
||||
if promptOf(provider.Anthropic, []byte(`not json`)) != nil {
|
||||
t.Fatal("a body that isn't one")
|
||||
}
|
||||
}
|
||||
|
||||
// Calibrated, the parts add up to what the vendor counted, and the one the
|
||||
// trace holds is left as it was.
|
||||
func TestPromptCalibrated(t *testing.T) {
|
||||
p := fixturePrompt(t, "claude-code.json", provider.Anthropic)
|
||||
before, _ := json.Marshal(p)
|
||||
c := p.calibrated(31337, 200000)
|
||||
sum := 0
|
||||
for _, x := range c.Parts {
|
||||
sum += x.Tokens
|
||||
}
|
||||
if !c.Counted || c.Tokens != 31337 || sum != 31337 || c.Window != 200000 {
|
||||
t.Fatalf("%+v (sum %d)", c, sum)
|
||||
}
|
||||
if after, _ := json.Marshal(p); string(after) != string(before) {
|
||||
t.Fatal("calibrating changed the estimate")
|
||||
}
|
||||
if again := c.calibrated(5, 0); again.Tokens != 31337 || again.Window != 200000 {
|
||||
t.Fatalf("counted twice %+v", again)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesOfCommand(t *testing.T) {
|
||||
for cmd, want := range map[string][]string{
|
||||
"cat main.go reconcile.go": {"main.go", "reconcile.go"},
|
||||
"sed -n 1,80p internal/x.go": {"internal/x.go"},
|
||||
"head -n 40 README.md": {"README.md"},
|
||||
`bash -lc "nl -ba a.go"`: {"a.go"},
|
||||
"cd /p && cat go.mod": {"go.mod"},
|
||||
"cat a.go | grep x": nil,
|
||||
"go test ./...": nil,
|
||||
"ls": nil,
|
||||
} {
|
||||
if got := filesOfCommand(cmd); !slices.Equal(got, want) {
|
||||
t.Errorf("%q: %q, want %q", cmd, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Through the gateway: the route says what its prompt held, scaled to
|
||||
// what the vendor counted, and the window of the model that answered.
|
||||
func TestRouteTellsItsPrompt(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(io.Discard, r.Body)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
io.WriteString(w, `{"id":"msg_1","type":"message","role":"assistant","model":"m1","content":[{"type":"text","text":"ok"}],
|
||||
"stop_reason":"end_turn","usage":{"input_tokens":1200,"cache_read_input_tokens":40000,"cache_creation_input_tokens":800,"output_tokens":9}}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
if err := provider.Save(provider.Provider{ID: "cw", Name: "CW", Anthropic: srv.URL, Models: []string{"m1"}, Key: "k",
|
||||
Contexts: map[string]int{"m1": 200000}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, _ := os.ReadFile("testdata/context/claude-code.json")
|
||||
var q map[string]any
|
||||
json.Unmarshal(b, &q)
|
||||
q["model"], q["stream"] = "cw/m1", false
|
||||
b, _ = json.Marshal(q)
|
||||
s := New()
|
||||
s.Handler().ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("POST", "/v1/messages", strings.NewReader(string(b))))
|
||||
rs := s.Trace(context.Background(), 0, 0).Routes
|
||||
if len(rs) != 1 || rs[0].Status != 200 {
|
||||
t.Fatalf("%+v", rs)
|
||||
}
|
||||
p := rs[0].Prompt
|
||||
if p == nil || !p.Counted || p.Tokens != 42000 || p.Window != 200000 {
|
||||
t.Fatalf("prompt %+v", p)
|
||||
}
|
||||
if _, ok := itemOf(p, PartMemory, "/home/dev/proj/CLAUDE.md"); !ok {
|
||||
t.Fatalf("memory %+v", p.Parts)
|
||||
}
|
||||
if rs[0].Conv == "" || !strings.HasPrefix(rs[0].Conv, "magpie-") {
|
||||
t.Fatalf("conversation %q", rs[0].Conv)
|
||||
}
|
||||
}
|
||||
+1131
File diff suppressed because it is too large.
Load diff
+527
@@ -0,0 +1,527 @@
|
||||
{
|
||||
"model": "gpt-6.1-sol",
|
||||
"input": [
|
||||
{
|
||||
"type": "additional_tools",
|
||||
"id": "at_fd3d86d3-93ce-5c3a-bcc9-30719648dfdf",
|
||||
"role": "developer",
|
||||
"tools": [
|
||||
{
|
||||
"type": "namespace",
|
||||
"name": "functions",
|
||||
"description": "",
|
||||
"tools": [
|
||||
{
|
||||
"type": "custom",
|
||||
"name": "exec",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing eli\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod \nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lor\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor \nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lor\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed \nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor s\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectet\n\nlorem ipsum dolor\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consect\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit s\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dol\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consecte\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tem\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lor\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet con\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmo\nlorem ipsum dolor sit amet consectetur adipiscing elit sed d\n\nlorem ipsum dolor\nlorem\nlorem ipsum dolor sit amet consec\nlorem ipsum dolor sit amet consectetur adi\nlorem ipsum dolor si\nlorem ipsum dolor si\nlorem ipsum dolor si\nlorem ipsum dolor sit am\nlo\nlorem ipsum d\nlorem ipsum do\nlorem ipsum dolor si\nlorem ipsum dolor s\nlorem ipsum dolor sit amet \nlo\nlorem ipsum dolor sit amet co\nlorem ipsum do\nlorem ipsum dolor si\nlorem ipsum dolor sit\nlorem ipsum dol\nlo\nlorem ipsum dolor sit amet co\nlorem ipsum do\nlorem ipsum dolor si\nlorem ipsum dolor sit\nlorem ipsum dol\nlo\nlorem ipsum dolor si\nlorem ipsum dol\nlorem ipsum dol\nlorem ipsum dolor sit amet c\nlorem ipsum dolor sit\nlo\nlorem ipsum dolor sit\nlorem ipsum dolo\nlorem ipsum dol\nlorem ipsum dolor s\nlorem ipsum dolor sit amet c\nlorem ipsum dolor sit\nlo\nlorem ipsum dolor sit\nlorem ipsum dolo\nlorem ipsum dol\nlorem ipsum dolor s\nlorem ipsum dolor sit amet c\nlorem ipsum dolor sit\nlo\nlorem ipsum dolor sit\nlorem ipsum dolor\nlorem ipsum dol\nlorem ipsum dolor\nlorem ipsum do\nlorem ipsum dolor sit a\nlorem ipsum dolor si\nlorem ipsum dolor sit amet c\nlorem ipsum dolo\nlorem ipsum dolor sit\nlorem ipsum dolor sit am\nlo\nlorem ipsum dolor sit ame\nlorem ipsum dolor s\nlorem ipsum dolor sit amet consectetur adipiscing elit s\nlorem ipsum dolor sit amet c\nlorem ipsum dolor sit\nlo\nlorem ipsum dolor s\nlorem ipsum dol\nlLine truncated
|
||||
"format": {
|
||||
"type": "grammar",
|
||||
"syntax": "lark",
|
||||
"definition": "\nstart: pragma_source | plain_source\npragma_source: PRAGMA_LINE NEWLINE SOURCE\nplain_source: SOURCE\n\nPRAGMA_LINE: /[ \\t]*\\/\\/ @exec:[^\\r\\n]*/\nNEWLINE: /\\r?\\n/\nSOURCE: /[\\s\\S]+/\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "wait",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmo\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tem\nlorem ipsum dolor sit amet consectetur adipiscing elit se\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet \nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum do\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor l\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adi\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lo\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dol",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cell_id": {
|
||||
"type": "string",
|
||||
"description": "Identifier of the running exec cell."
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiu"
|
||||
},
|
||||
"terminate": {
|
||||
"type": "boolean",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmo"
|
||||
},
|
||||
"yield_time_ms": {
|
||||
"type": "number",
|
||||
"description": "Wait before yielding more output. Defaults to 10000 ms."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cell_id"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "request_user_input",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adip",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"questions": {
|
||||
"type": "array",
|
||||
"description": "Questions to show the user. Prefer 1 and do not exceed 3",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"header": {
|
||||
"type": "string",
|
||||
"description": "Short header label shown in the UI (12 or fewer chars)."
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Stable identifier for mapping answers (snake_case)."
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod ",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "One short sentence explaining impact/tradeoff if selected."
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "User-facing label (1-5 words)."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"label",
|
||||
"description"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"question": {
|
||||
"type": "string",
|
||||
"description": "Single-sentence prompt shown to the user."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"header",
|
||||
"question",
|
||||
"options"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"questions"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "request_user_input_async",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"questions": {
|
||||
"type": "array",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempo",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "array",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolo",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem "
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"minItems": 1
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"questions"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "namespace",
|
||||
"name": "clock",
|
||||
"description": "Tools for reading and waiting on time.",
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "sleep",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eius",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"duration_ms": {
|
||||
"type": "number",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eius"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"duration_ms"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "namespace",
|
||||
"name": "collaboration",
|
||||
"description": "Tools for spawning and managing sub-agents.",
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "followup_task",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor l",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Message text to send to the target agent.",
|
||||
"encrypted": true
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lo"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target",
|
||||
"message"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "interrupt_agent",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed ",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do ei"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "list_agents",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path_prefix": {
|
||||
"type": "string",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lo"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "send_message",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet c",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Message text to queue on the target agent.",
|
||||
"encrypted": true
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do "
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target",
|
||||
"message"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "spawn_agent",
|
||||
"description": "\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem i\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eius\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusm\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiu\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipi\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod te\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolo\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscin\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet co",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fork_turns": {
|
||||
"type": "string",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod t"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Initial plain-text task for the new agent.",
|
||||
"encrypted": true
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor "
|
||||
},
|
||||
"reasoning_effort": {
|
||||
"type": "string",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lo"
|
||||
},
|
||||
"task_name": {
|
||||
"type": "string",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_name",
|
||||
"message"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "wait_agent",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempo",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timeout_ms": {
|
||||
"type": "number",
|
||||
"description": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusm"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_65f2d68b-9dd2-5b1a-9e9e-84456c726485",
|
||||
"role": "developer",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum d\n\n# When to ask the user for permission\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur \n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmo\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing el\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do \n\n# Autonomy and persistence\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do ei\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipisc\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusm\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet cons\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet \n\n# Personality\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet c\n\nlorem ipsum dolo\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor \n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sedLine truncated
|
||||
}
|
||||
],
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"content_item_kinds": [
|
||||
"model.base_instructions"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_01a1179d-34a3-7cb1-b23c-fa807225131e",
|
||||
"role": "developer",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "<skills_instructions>\nlorem ips\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet con\nlorem ipsum dol\nlorem ipsum dolor sit amet consecte\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor si\nlorem ipsum dolor si\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod te\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipi\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lor\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet co\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit se\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lore\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ips\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dol\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor \nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consect\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod t\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur Line truncated
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "<permissions instructions>\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum \nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adi\n</permissions instructions>"
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "lorem ipsum dolor sit amet consectetur adipiscing\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet conse\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum d\n\nlorem ipsum dolor sit amet consect\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing e\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing e\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit\n</collaboration_mode>"
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "<recommended_plugins>\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do e\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod \nlorem ipsum dolor sit amet consec\nlorem ipsum dolor sit amet consectetur adipiscing elit \nlorem ipsum dolor sit amet consectetu\nlorem ipsum dolor sit amet consectetur adip\nlorem ipsum dolor sit amet consectetur \nlorem ipsum dolor sit amet consectetu\nlorem ipsum dolor sit amet consectetur adipiscing elit se\nlorem ipsum dolor sit amet consectetur adipiscing e\nlorem ipsum dolor sit amet consectetur \nlorem ipsum dolor sit amet consectetur \nlorem ipsum dolor sit amet consectetur adipiscing elit sed do\nlorem ipsum dolor sit amet consectetur adipiscing elit sed \nlorem ipsum dolor sit amet consectetur adipiscing eli\nlorem ipsum dolor sit amet consectetur adipisci\nlorem ipsum dolor sit amet consectetu\nlorem ipsum dolor sit amet consectetu\n</recommended_plugins>"
|
||||
}
|
||||
],
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "01a1179d-1e8a-7302-b05b-64d89e8f60d3",
|
||||
"create_time": 1791397541.027822,
|
||||
"content_item_kinds": [
|
||||
"host_skills.instructions",
|
||||
"permissions.instructions",
|
||||
"collaboration_mode.instructions",
|
||||
"plugins.recommendations"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_01a1179d-34a3-7cb1-b23c-fa9ee963c199",
|
||||
"role": "developer",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur\n\nlorem ipsum dolor sit amet consectetur adipiscing el\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor si\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing \nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing \nlorem ipsum dolor sit amet consectetur adipiscing\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet c\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do \nlor\nlorem ipsum dolor sit amet consectet\nlorem ipsum dolor sit \nlorem ipsum dolo\nlorem ip\n<payload text>\nlor\nlorem ipsum dolor sit amet consec\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adi\n\nlorem ipsum dolor sit amet consectetur adipisci\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod\nlorem ipsum dolor sit amet consectetur adipiscing el\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem \n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lo\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet co\n\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing "
|
||||
}
|
||||
],
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "01a1179d-1e8a-7302-b05b-64d89e8f60d3",
|
||||
"create_time": 1791397541.027822,
|
||||
"content_item_kinds": [
|
||||
"multi_agent.role_instructions"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_01a1179d-34a3-7cb1-b23c-faaa76c53fb7",
|
||||
"role": "developer",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur a"
|
||||
}
|
||||
],
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "01a1179d-1e8a-7302-b05b-64d89e8f60d3",
|
||||
"create_time": 1791397541.027823,
|
||||
"content_item_kinds": [
|
||||
"multi_agent.mode_instructions"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_01a1179d-34a3-7cb1-b23c-fabfd8b52251",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "# AGENTS.md instructions for /home/dev/proj\n\n<INSTRUCTIONS>\n# ledger\nlorem ipsum dolor sit amet consectetur adipiscing\n\n</INSTRUCTIONS>"
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "<environment_context>\nlorem ipsum dolor sit amet \nlorem ipsum dolor si\nlorem ipsum dolor sit amet consectetur ad\nlorem ipsum dolor sit amet consectet\nlorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum \n</environment_context>"
|
||||
}
|
||||
],
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "01a1179d-1e8a-7302-b05b-64d89e8f60d3",
|
||||
"create_time": 1791397541.027823,
|
||||
"content_item_kinds": [
|
||||
"agents_md.instructions",
|
||||
"environments.environment_context"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_01a1179d-34b9-7533-92c0-a5b358d2cca5",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor"
|
||||
}
|
||||
],
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "01a1179d-1e8a-7302-b05b-64d89e8f60d3",
|
||||
"create_time": 1791397541.049139,
|
||||
"content_item_kinds": [
|
||||
"user.text"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_00f7990330b08aa5016ac68eb801a081918c22be6b703f6e0c",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tem"
|
||||
}
|
||||
],
|
||||
"phase": "commentary",
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "01a1179d-1e8a-7302-b05b-64d89e8f60d3",
|
||||
"create_time": 1791397560.0169764,
|
||||
"content_item_kinds": [
|
||||
"unknown"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom_tool_call",
|
||||
"id": "ctc_00f7990330b08aa5016ac68eb8813c81919d5717e37af30804",
|
||||
"status": "completed",
|
||||
"call_id": "call_f9b38b8832494d37a213abde58ee8af2",
|
||||
"name": "exec",
|
||||
"input": "text(await tools.exec_command({cmd:\"cat main.go reconcile.go\",max_output_tokens:8000}));\n",
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "01a1179d-1e8a-7302-b05b-64d89e8f60d3",
|
||||
"create_time": 1791397560.5094378
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom_tool_call_output",
|
||||
"id": "ctco_01a1179d-8c8f-7521-b8f4-32635917b555",
|
||||
"call_id": "call_f9b38b8832494d37a213abde58ee8af2",
|
||||
"output": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "Script completed\nWall time 0.1 seconds\nOutput:\n"
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempo"
|
||||
}
|
||||
],
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "01a1179d-1e8a-7302-b05b-64d89e8f60d3",
|
||||
"create_time": 1791397563.535103
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"parallel_tool_calls": false,
|
||||
"reasoning": {
|
||||
"effort": "low",
|
||||
"context": "all_turns"
|
||||
},
|
||||
"store": false,
|
||||
"stream": true,
|
||||
"include": [
|
||||
"reasoning.encrypted_content"
|
||||
],
|
||||
"text": {
|
||||
"verbosity": "low"
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,12 @@ type Route struct {
|
||||
Routed bool `json:"routed,omitempty"`
|
||||
// Upstream: the provider an aggregator said answered behind it
|
||||
Upstream string `json:"upstream,omitempty"`
|
||||
// Prompt: what the prompt held, part by part, and the window it went
|
||||
// into (prompt.go)
|
||||
Prompt *Prompt `json:"prompt,omitempty"`
|
||||
// Conv: the conversation a request without a session id is part of,
|
||||
// as a digest of its first user turn (conversationID)
|
||||
Conv string `json:"conv,omitempty"`
|
||||
}
|
||||
|
||||
// RouteUsage is one billable attempt's pricing inputs, kept in routing history.
|
||||
|
||||
@@ -12356,6 +12356,7 @@ async function loadUsage(asked) {
|
||||
if (asked) loadQuotas(true);
|
||||
if (usageTab === "sessions") return loadSessions();
|
||||
if (usageTab === "requests") return loadLedger();
|
||||
if (usageTab === "context") return window.loadContext();
|
||||
renderUsageLoading();
|
||||
if (!asked) loadQuotas();
|
||||
const p = period, read = ++usageRead;
|
||||
@@ -15751,7 +15752,7 @@ function renderLedger() {
|
||||
// The agents' own sessions, read from their session files: what each cost,
|
||||
// and the command that picks it up again. A segment of the Usage page.
|
||||
|
||||
const USAGE_TABS = [["usage", "Overview"], ["requests", "Requests"], ["sessions", "Sessions"]];
|
||||
const USAGE_TABS = [["usage", "Overview"], ["requests", "Requests"], ["sessions", "Sessions"], ["context", "Context"]];
|
||||
let usageTab = "usage";
|
||||
let quotaFocus = ""; // pending provider/account card requested by the menu bar
|
||||
let quotaFocusUntil = 0; // expire before a late quota response can move the reader
|
||||
@@ -15821,11 +15822,13 @@ function renderUsageTab() {
|
||||
}
|
||||
slide(seg, "usageTab");
|
||||
const on = usageTab === "sessions";
|
||||
$("#period").hidden = on;
|
||||
// the Context tab has its own range, in its pane
|
||||
$("#period").hidden = on || usageTab === "context";
|
||||
$("#sessRange").hidden = !on;
|
||||
$("#usagePane").hidden = usageTab !== "usage";
|
||||
$("#ledgerPane").hidden = usageTab !== "requests";
|
||||
$("#sessionsPane").hidden = !on;
|
||||
$("#contextPane").hidden = usageTab !== "context";
|
||||
}
|
||||
|
||||
const sessDays = () => SESS_RANGES.find(([id]) => id === sessRange)[2];
|
||||
@@ -20090,6 +20093,8 @@ function refreshUsage(now = false) {
|
||||
} else if (usageTab === "requests") {
|
||||
// the newest page takes the requests as they come; an older one stays put
|
||||
if (ledger && (want || !ledOffset)) await loadLedger(true);
|
||||
} else if (usageTab === "context") {
|
||||
await window.loadContext();
|
||||
} else {
|
||||
// the reader asking reads the allowances afresh, a Claude account's by
|
||||
// running Claude Code's own /usage (the backend runs it at most once in 30s)
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/* Context windows: a request's prompt as a card (Routing, the Usage page's
|
||||
Context tab) and each agent's score and tags. */
|
||||
:root {
|
||||
--k-system: var(--c6); --k-tools: var(--c2); --k-memory: var(--c4);
|
||||
--k-files: var(--c3); --k-results: var(--c5); --k-chat: var(--c1);
|
||||
--k-free: color-mix(in srgb, var(--fg) 7%, transparent);
|
||||
}
|
||||
.k-system { --k: var(--k-system); }
|
||||
.k-tools { --k: var(--k-tools); }
|
||||
.k-memory { --k: var(--k-memory); }
|
||||
.k-files { --k: var(--k-files); }
|
||||
.k-results { --k: var(--k-results); }
|
||||
.k-chat { --k: var(--k-chat); }
|
||||
i.dot { display: inline-block; flex: none; width: 8px; height: 8px; border-radius: 2.5px; background: var(--k); }
|
||||
i.dot.free { background: var(--k-free); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 10%, transparent); }
|
||||
|
||||
.ctx-card { position: relative; container-type: inline-size; background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px 12px; display: flex; flex-direction: column; gap: 14px; min-width: 0; font-variant-numeric: tabular-nums; }
|
||||
.ctx-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.ctx-head .grow, .ctx-foot .grow, .ctx-contents-head .grow, .ctx-agent-head .grow, .ctx-agent-foot .grow, .ctx-tools .grow, .ctx-sess-head .grow { flex: 1; }
|
||||
.ctx-title { font-size: 13px; font-weight: 600; color: var(--fg); white-space: nowrap; }
|
||||
.ctx-model { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--code-font, var(--code)); font-size: 11px; color: var(--muted); }
|
||||
.ctx-state { display: inline-flex; align-items: center; gap: 5px; flex: none; height: 20px; padding: 0 8px; border-radius: 10px; font-size: 11px; font-weight: 500; background: var(--pill); color: var(--fg-2); }
|
||||
.ctx-state i { width: 6px; height: 6px; border-radius: 50%; background: var(--faint); }
|
||||
.ctx-state.counted i { background: var(--green); }
|
||||
.ctx-state.live { background: var(--green-soft); color: var(--green); }
|
||||
.ctx-state.live i { background: var(--green); animation: ctx-pulse 1.4s ease-in-out infinite; }
|
||||
@keyframes ctx-pulse { 50% { opacity: .35; transform: scale(.8); } }
|
||||
|
||||
.ctx-k { display: flex; align-items: center; gap: 8px; font-size: 10.5px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
|
||||
:root[lang="zh"] .ctx-k, :root[lang="ja"] .ctx-k { letter-spacing: .02em; }
|
||||
.ctx-top { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 12px 28px; }
|
||||
.ctx-top > :only-child { grid-column: 1 / -1; }
|
||||
.ctx-stat { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
||||
.ctx-big { display: flex; align-items: baseline; gap: 0; white-space: nowrap; color: var(--muted); font-size: 13px; }
|
||||
.ctx-big b { font-size: 28px; line-height: 1.1; font-weight: 600; letter-spacing: -.02em; color: var(--fg); }
|
||||
.ctx-sub { font-size: 10.5px; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
:root[lang="zh"] .ctx-sub, :root[lang="ja"] .ctx-sub { letter-spacing: .02em; }
|
||||
.ctx-health { height: 17px; display: inline-flex; align-items: center; padding: 0 7px; border-radius: 9px; font-size: 10px; letter-spacing: .03em; }
|
||||
.ctx-health.ok { background: var(--green-soft); color: var(--green); }
|
||||
.ctx-health.warn { background: var(--amber-soft); color: var(--amber); }
|
||||
.ctx-health.bad { background: var(--red-soft); color: var(--red); }
|
||||
.ctx-bar { position: relative; display: block; height: 6px; border-radius: 3px; background: var(--k-free); overflow: hidden; }
|
||||
.ctx-bar i { position: absolute; inset: 0 auto 0 0; border-radius: 3px; background: var(--green); transform-origin: left; animation: ctx-grow .7s var(--ease, ease) both; }
|
||||
.ctx-cache .ctx-bar { margin: 3px 0 2px; }
|
||||
@keyframes ctx-grow { from { transform: scaleX(0); } }
|
||||
|
||||
/* the window, a cell for each 1/400 of it */
|
||||
.ctx-waffle { display: grid; grid-template-columns: repeat(50, minmax(0, 1fr)); gap: 2px; }
|
||||
@container (max-width: 520px) { .ctx-waffle { grid-template-columns: repeat(25, minmax(0, 1fr)); } }
|
||||
.ctx-waffle > i { aspect-ratio: 1; border-radius: 2px; background: var(--k, var(--k-free)); transition: opacity .16s, transform .16s; animation: ctx-cell .32s var(--ease, ease) both; animation-delay: var(--d, 0ms); }
|
||||
.ctx-waffle > i.free { --k: var(--k-free); }
|
||||
@keyframes ctx-cell { from { opacity: 0; transform: scale(.4); } }
|
||||
.ctx-waffle[data-f] > i { opacity: .16; }
|
||||
.ctx-waffle[data-f="system"] > i.k-system, .ctx-waffle[data-f="tools"] > i.k-tools,
|
||||
.ctx-waffle[data-f="memory"] > i.k-memory, .ctx-waffle[data-f="files"] > i.k-files,
|
||||
.ctx-waffle[data-f="results"] > i.k-results, .ctx-waffle[data-f="chat"] > i.k-chat,
|
||||
.ctx-waffle[data-f="free"] > i.free, .ctx-waffle[data-f="item"] > i.on { opacity: 1; }
|
||||
.ctx-waffle[data-f="item"] > i.on { transform: scale(1.12); }
|
||||
.ctx-tip { position: absolute; z-index: 5; min-width: 190px; max-width: 280px; padding: 8px 10px; border-radius: 9px; background: #17171b; color: #ededf1; box-shadow: 0 10px 30px rgba(0, 0, 0, .28); font-size: 11.5px; pointer-events: none; display: flex; flex-direction: column; gap: 3px; }
|
||||
.ctx-tip[hidden] { display: none; }
|
||||
.ctx-tip-head { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; }
|
||||
.ctx-tip-head .grow { flex: 1; }
|
||||
.ctx-tip-head span:last-child { color: #a1a1aa; }
|
||||
.ctx-tip-row { display: flex; gap: 10px; color: #b9b9c1; }
|
||||
.ctx-tip-row .n { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ctx-tip-row.on { color: #fff; font-weight: 500; }
|
||||
.ctx-tip-more { color: #85858d; font-size: 10.5px; }
|
||||
|
||||
.ctx-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: -4px; }
|
||||
.ctx-leg { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--fg-2); cursor: default; padding: 2px 0; }
|
||||
.ctx-leg .v { color: var(--muted); }
|
||||
.ctx-leg:hover .n { color: var(--fg); }
|
||||
|
||||
.ctx-contents { display: flex; flex-direction: column; gap: 6px; padding-top: 12px; border-top: 1px solid var(--line); }
|
||||
.ctx-contents-head { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.ctx-contents-head .segs { max-width: 100%; overflow-x: auto; scrollbar-width: none; flex: none; }
|
||||
.ctx-contents-head .segs .opt { height: 20px; padding: 0 9px; font-size: 11px; white-space: nowrap; flex: none; }
|
||||
@container (max-width: 520px) { .ctx-contents-head { flex-wrap: wrap; } .ctx-contents-head .segs { flex: 1 1 100%; } }
|
||||
.ctx-rows { display: flex; flex-direction: column; }
|
||||
.ctx-row { display: flex; align-items: center; gap: 8px; min-height: 26px; padding: 0 6px; margin: 0 -6px; border-radius: 6px; font-size: 12px; }
|
||||
.ctx-row:hover { background: var(--pill); }
|
||||
.ctx-name { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 6px; overflow: hidden; white-space: nowrap; }
|
||||
.ctx-name b { font-weight: 500; color: var(--fg); overflow: hidden; text-overflow: ellipsis; flex: none; max-width: 100%; }
|
||||
.ctx-name small { color: var(--faint); font-size: 11px; overflow: hidden; text-overflow: ellipsis; direction: rtl; text-align: left; min-width: 0; }
|
||||
.ctx-x { font-size: 11px; color: var(--muted); }
|
||||
.ctx-tag { flex: none; font-family: var(--code-font, var(--code)); font-size: 10px; padding: 1px 5px; border-radius: 4px; background: var(--pill); color: var(--muted); }
|
||||
.ctx-mini { flex: none; width: 72px; height: 4px; border-radius: 2px; background: var(--k-free); overflow: hidden; }
|
||||
.ctx-mini i { display: block; height: 100%; border-radius: 2px; background: var(--k); }
|
||||
.ctx-v { flex: none; width: 48px; text-align: right; color: var(--fg-2); font-size: 11.5px; }
|
||||
@container (max-width: 420px) { .ctx-mini, .ctx-tag { display: none; } }
|
||||
.ctx-empty { font-size: 12px; color: var(--muted); padding: 4px 0; }
|
||||
.ctx-more { align-self: flex-start; margin-top: 2px; font-size: 11.5px; }
|
||||
|
||||
.ctx-foot { display: flex; align-items: center; gap: 10px; min-width: 0; font-size: 11px; color: var(--muted); }
|
||||
.ctx-crumbs { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--code-font, var(--code)); font-size: 10.5px; color: var(--muted); }
|
||||
.ctx-note { white-space: nowrap; flex: none; }
|
||||
.ctx-session { display: flex; align-items: center; gap: 12px; margin-top: -4px; }
|
||||
.ctx-session .ctx-spark { flex: 1; height: 34px; }
|
||||
.ctx-spark { position: relative; display: block; width: 100%; }
|
||||
.ctx-spark svg { display: block; width: 100%; height: 100%; overflow: visible; }
|
||||
.ctx-spark .at { position: absolute; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; border-radius: 50%; background: var(--card); border: 1.6px solid var(--k-chat); box-sizing: border-box; pointer-events: none; }
|
||||
.ctx-spark .win { stroke: var(--line); stroke-dasharray: 3 3; stroke-width: 1; vector-effect: non-scaling-stroke; }
|
||||
.ctx-spark .area { fill: color-mix(in srgb, var(--k-chat) 14%, transparent); }
|
||||
.ctx-spark .line { fill: none; stroke: var(--k-chat); stroke-width: 1.5; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
|
||||
.ctx-spark .hit { fill: transparent; cursor: pointer; }
|
||||
.ctx-spark .hit:hover { fill: color-mix(in srgb, var(--fg) 7%, transparent); }
|
||||
|
||||
/* Routing: the card under the story of the request */
|
||||
.rt-log .ctx-card { margin-top: 12px; }
|
||||
|
||||
/* the Usage page's Context tab */
|
||||
.ctx-agents { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr)); gap: 12px; margin: 4px 0 18px; }
|
||||
.ctx-agent { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px 12px; display: flex; flex-direction: column; gap: 12px; min-width: 0; font-variant-numeric: tabular-nums; }
|
||||
.ctx-agent-head { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.ctx-who-box { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
||||
.ctx-who { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.ctx-who .ic { width: 20px; height: 20px; flex: none; }
|
||||
.ctx-who b { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ctx-who-sub { font-size: 11px; color: var(--muted); }
|
||||
.ctx-grade { display: flex; flex-direction: column; align-items: center; gap: 2px; flex: none; }
|
||||
.ctx-grade-word { font-size: 10.5px; font-weight: 600; }
|
||||
.ctx-ring { position: relative; width: 52px; height: 52px; --tone: var(--accent); }
|
||||
.ctx-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
|
||||
.ctx-ring circle { fill: none; stroke-width: 5; stroke-linecap: round; }
|
||||
.ctx-ring .track { stroke: var(--k-free); }
|
||||
.ctx-ring .arc { stroke: var(--tone); stroke-dashoffset: var(--to); animation: ctx-arc .9s var(--ease, ease) both; }
|
||||
.ctx-pane.still .ctx-waffle > i, .ctx-pane.still .ctx-bar i, .ctx-pane.still .ctx-ring .arc { animation: none; }
|
||||
@keyframes ctx-arc { from { stroke-dashoffset: 163.36; } }
|
||||
.ctx-ring b { position: absolute; inset: 0; display: grid; place-items: center; font-size: 16px; font-weight: 650; letter-spacing: -.02em; }
|
||||
.ctx-ring.great, .ctx-grade-word.great { --tone: var(--green); color: var(--green); }
|
||||
.ctx-ring.good, .ctx-grade-word.good { --tone: var(--c2); color: var(--c2); }
|
||||
.ctx-ring.fair, .ctx-grade-word.fair { --tone: var(--amber); color: var(--amber); }
|
||||
.ctx-ring.poor, .ctx-grade-word.poor { --tone: var(--red); color: var(--red); }
|
||||
.ctx-ring b { color: var(--fg); }
|
||||
.ctx-scores { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 18px; }
|
||||
.ctx-score { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 8px; font-size: 11.5px; color: var(--fg-2); cursor: default; }
|
||||
.ctx-score .ctx-bar { height: 4px; }
|
||||
.ctx-score .ctx-bar i { background: var(--green); }
|
||||
.ctx-score.warn .ctx-bar i { background: var(--amber); }
|
||||
.ctx-score.bad .ctx-bar i { background: var(--red); }
|
||||
.ctx-score .v { color: var(--muted); font-size: 10.5px; }
|
||||
.ctx-tags { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.ctx-tags:empty { display: none; }
|
||||
.ctx-chip { display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 9px; border-radius: 11px; font-size: 11.5px; font-weight: 500; background: var(--pill); color: var(--fg-2); cursor: default; }
|
||||
.ctx-chip i { width: 6px; height: 6px; border-radius: 50%; background: var(--c7); }
|
||||
.ctx-chip.good { background: var(--green-soft); color: var(--green); }
|
||||
.ctx-chip.good i { background: var(--green); }
|
||||
.ctx-chip.warn { background: var(--amber-soft); color: var(--amber); }
|
||||
.ctx-chip.warn i { background: var(--amber); }
|
||||
.ctx-chip.info i { background: var(--accent); }
|
||||
.ctx-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px 12px; padding: 10px 0 2px; border-top: 1px solid var(--line); }
|
||||
.ctx-fact { display: flex; flex-direction: column; gap: 2px; min-width: 0; cursor: default; }
|
||||
.ctx-fact b { font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
|
||||
.ctx-fact .ctx-k { font-size: 9.5px; overflow: hidden; text-overflow: ellipsis; display: block; }
|
||||
.ctx-mix { display: flex; flex-direction: column; gap: 6px; }
|
||||
.ctx-share { display: flex; height: 8px; border-radius: 4px; overflow: hidden; gap: 2px; background: var(--k-free); }
|
||||
.ctx-share i { display: block; min-width: 3px; background: var(--k); }
|
||||
.ctx-mix-keys { display: flex; flex-wrap: wrap; gap: 2px 12px; font-size: 11px; color: var(--muted); }
|
||||
.ctx-mix-keys span { display: inline-flex; align-items: center; gap: 5px; }
|
||||
.ctx-agent-foot { display: flex; align-items: center; gap: 8px; min-width: 0; margin-top: auto; }
|
||||
.ctx-models { display: flex; flex-wrap: wrap; gap: 4px; min-width: 0; }
|
||||
.ctx-models code { font-family: var(--code-font, var(--code)); font-size: 10.5px; padding: 2px 6px; border-radius: 5px; background: var(--pill); color: var(--fg-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
||||
.ctx-agent-foot .text { flex: none; font-size: 11.5px; }
|
||||
|
||||
.ctx-sess-head { margin-top: 6px; }
|
||||
.ctx-sess-head .segs .opt { height: 20px; padding: 0 9px; font-size: 11px; }
|
||||
.ctx-sess-list { display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
|
||||
.ctx-sess + .ctx-sess { border-top: 1px solid var(--line-2, var(--line)); }
|
||||
.ctx-sess-row { all: unset; box-sizing: border-box; width: 100%; display: grid; grid-template-columns: 20px minmax(0, 1.3fr) minmax(80px, 1fr) 44px minmax(0, auto) 92px; align-items: center; gap: 12px; padding: 8px 14px; cursor: pointer; font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||
.ctx-sess-row:hover { background: var(--pill); }
|
||||
.ctx-sess-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
||||
.ctx-sess-row .ic { width: 18px; height: 18px; }
|
||||
.ctx-sess-name { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
|
||||
.ctx-sess-name b { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ctx-sess-name b.raw { font-family: var(--code-font, var(--code)); font-weight: 500; letter-spacing: .02em; }
|
||||
.ctx-sess-name code { font-family: var(--code-font, var(--code)); font-size: 10.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ctx-sess-row .ctx-spark { height: 24px; }
|
||||
.ctx-sess-peak { justify-self: end; font-weight: 600; }
|
||||
.ctx-sess-peak.ok { color: var(--green); }
|
||||
.ctx-sess-peak.warn { color: var(--amber); }
|
||||
.ctx-sess-peak.bad { color: var(--red); }
|
||||
.ctx-sess-meta, .ctx-sess-when { color: var(--muted); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ctx-sess-when { text-align: right; }
|
||||
.ctx-sess.open { background: var(--card-2); }
|
||||
.ctx-sess.open > .ctx-card { margin: 0 12px 12px; }
|
||||
@media (max-width: 640px) {
|
||||
.ctx-sess-row { grid-template-columns: 20px minmax(0, 1fr) 64px 40px; }
|
||||
.ctx-sess-meta, .ctx-sess-when { display: none; }
|
||||
.ctx-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.ctx-scores { grid-template-columns: 1fr; }
|
||||
.ctx-top { grid-template-columns: 1fr; }
|
||||
}
|
||||
.ctx-none { display: flex; flex-direction: column; gap: 4px; align-items: center; padding: 48px 16px; color: var(--muted); font-size: 12.5px; text-align: center; background: var(--card); border: 1px dashed var(--line); border-radius: 14px; }
|
||||
.ctx-none b { color: var(--fg); font-size: 13.5px; font-weight: 600; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ctx-waffle > i, .ctx-bar i, .ctx-ring .arc, .ctx-state.live i { animation: none; }
|
||||
.ctx-waffle > i { transition: none; }
|
||||
}
|
||||
|
||||
/* Safari 15: no container queries, so the card's narrow layout goes by the
|
||||
window's width; no color-mix, so the free cells and the line's area are
|
||||
tinted by opacity */
|
||||
@supports not (container-type: inline-size) {
|
||||
@media (max-width: 560px) {
|
||||
.ctx-waffle { grid-template-columns: repeat(25, minmax(0, 1fr)); }
|
||||
.ctx-contents-head { flex-wrap: wrap; }
|
||||
.ctx-contents-head .segs { flex: 1 1 100%; }
|
||||
}
|
||||
@media (max-width: 460px) { .ctx-mini, .ctx-tag { display: none; } }
|
||||
}
|
||||
@supports not (color: color-mix(in srgb, red 50%, blue)) {
|
||||
:root { --k-free: rgba(128, 128, 128, .12); }
|
||||
i.dot.free { box-shadow: inset 0 0 0 1px var(--line); }
|
||||
.ctx-spark .area { fill: var(--k-chat); fill-opacity: .14; }
|
||||
.ctx-spark .hit:hover { fill: var(--line); }
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
"use strict";
|
||||
// Context windows: what a request's prompt holds, as the gateway read it
|
||||
// (gateway.Route.Prompt), drawn as a card — how full the window is, how
|
||||
// much the vendor read from its cache, a grid of the window with a cell
|
||||
// for each 1/400 of it coloured by what fills it, and the contents part by
|
||||
// part. Routing shows the card for the request it tells of; the Usage
|
||||
// page's Context tab scores each agent's use of its window, tags it, and
|
||||
// lists its sessions' fill request by request.
|
||||
(() => {
|
||||
const PARTS = ["system", "tools", "memory", "files", "results", "chat"];
|
||||
const PART_NAMES = {
|
||||
system: "System prompt", tools: "Tools", memory: "Memory",
|
||||
files: "Files", results: "Tool results", chat: "Conversation",
|
||||
};
|
||||
const PART_NOTES = {
|
||||
system: "The agent's own instructions",
|
||||
tools: "The definitions of the tools it can call",
|
||||
memory: "Instruction files, memory and reminders",
|
||||
files: "Files it read, and images",
|
||||
results: "What the other tools it called returned",
|
||||
chat: "What you and the model said, turn by turn",
|
||||
};
|
||||
const CELLS = 400;
|
||||
const partName = (k) => t(PART_NAMES[k] || k);
|
||||
|
||||
const fmtK = (n) => {
|
||||
n = Math.max(0, Math.round(n || 0));
|
||||
const short = (x, d) => x.toFixed(d).replace(/\.0+$/, "");
|
||||
if (n >= 1e6) return short(n / 1e6, n >= 1e7 ? 1 : 2) + "M";
|
||||
if (n >= 1e3) return short(n / 1e3, n >= 1e5 ? 0 : 1) + "K";
|
||||
return String(n);
|
||||
};
|
||||
const pct = (x) => Math.round(x * 100) + "%";
|
||||
const median = (xs) => {
|
||||
if (!xs.length) return 0;
|
||||
const s = [...xs].sort((a, b) => a - b);
|
||||
return s[s.length >> 1];
|
||||
};
|
||||
const agentMeta = (id) => (state.clients || state.agents || []).find((a) => a.id === id);
|
||||
const agentLabel = (id) => agentMeta(id)?.name || id || t("your agent");
|
||||
const agentIcon = (id) => icon(agentMeta(id)?.icon || "generic");
|
||||
|
||||
// what an item of the prompt is called: a file's path or a tool's name
|
||||
// as it is; magpie's own names for the rest in the reader's language
|
||||
const ITEM_NAMES = {
|
||||
prompt: "Agent prompt", harness: "Harness messages", billing: "Billing header",
|
||||
environment: "Environment notes", context: "Context", skills: "Skills",
|
||||
reminders: "System reminders", compacted: "Compacted summary", image: "Image", document: "Document",
|
||||
};
|
||||
function itemName(it) {
|
||||
if (it.tag === "turn") return t("Turn {n}", { n: it.name });
|
||||
if (it.tag === "turns") return t("{n} earlier turns", { n: it.n });
|
||||
if (it.tag === "more") return t("{n} more", { n: it.n });
|
||||
return ITEM_NAMES[it.name] ? t(ITEM_NAMES[it.name]) : it.name;
|
||||
}
|
||||
// a path's file name first, where it is after it
|
||||
function itemLabel(it, kind) {
|
||||
const name = itemName(it);
|
||||
const e = el("span", "ctx-name");
|
||||
if ((kind === "files" || kind === "memory") && name.includes("/")) {
|
||||
const at = name.lastIndexOf("/");
|
||||
e.append(el("b", "", name.slice(at + 1)), el("small", "", name.slice(0, at + 1)));
|
||||
} else e.append(el("b", "", name));
|
||||
e.title = name;
|
||||
return e;
|
||||
}
|
||||
const TAG_NAMES = {
|
||||
mcp: "MCP", namespace: "namespace", read: "read", shell: "shell", result: "result",
|
||||
user: "user", project: "project", local: "local", memory: "memory", skills: "skills",
|
||||
reminder: "reminder", image: "image", turn: "", turns: "", more: "", compacted: "",
|
||||
};
|
||||
const tagOf = (it) => it.tag in TAG_NAMES ? TAG_NAMES[it.tag] : it.tag || "";
|
||||
|
||||
const health = (fill) => fill < 0.75 ? ["ok", "Healthy"] : fill < 0.9 ? ["warn", "Filling up"] : ["bad", "Near full"];
|
||||
|
||||
// the cache a route's vendor read: its last answer's usage
|
||||
function cacheOf(r) {
|
||||
const u = r.usage?.[r.usage.length - 1];
|
||||
if (!u) return null;
|
||||
const total = (u.in || 0) + (u.cache_read || 0) + (u.cache_write || 0);
|
||||
if (!total) return null;
|
||||
return { read: u.cache_read || 0, total };
|
||||
}
|
||||
|
||||
// the cells of the grid, each the part and item it shows; a window
|
||||
// smaller than the prompt (not known, or overrun) is the prompt
|
||||
function cellsOf(p, window) {
|
||||
const scale = Math.max(window, p.tokens, 1);
|
||||
const used = Math.min(CELLS, Math.round((p.tokens / scale) * CELLS));
|
||||
const parts = PARTS.map((k) => p.parts.find((x) => x.kind === k)).filter((x) => x && x.tokens > 0);
|
||||
const sum = parts.reduce((s, x) => s + x.tokens, 0) || 1;
|
||||
// largest remainder, so the cells add up to used and a part of any
|
||||
// size has at least one
|
||||
const want = parts.map((x) => (x.tokens / sum) * used);
|
||||
const got = want.map((w) => Math.max(1, Math.floor(w)));
|
||||
let left = used - got.reduce((s, n) => s + n, 0);
|
||||
const order = want.map((w, i) => [w - Math.floor(w), i]).sort((a, b) => b[0] - a[0]);
|
||||
for (let j = 0; left > 0 && order.length; j = (j + 1) % order.length, left--) got[order[j][1]]++;
|
||||
for (let j = got.length - 1; left < 0 && j >= 0; j--) while (left < 0 && got[j] > 1) { got[j]--; left++; }
|
||||
const cells = [];
|
||||
parts.forEach((x, pi) => {
|
||||
const items = [...(x.items || [])].sort((a, b) => b.tokens - a.tokens);
|
||||
const isum = items.reduce((s, it) => s + it.tokens, 0) || 1;
|
||||
let at = 0, acc = 0;
|
||||
for (let c = 0; c < got[pi]; c++) {
|
||||
// the item whose share the cell's middle falls in
|
||||
const mid = ((c + 0.5) / got[pi]) * isum;
|
||||
while (at < items.length - 1 && acc + items[at].tokens < mid) acc += items[at++].tokens;
|
||||
cells.push({ kind: x.kind, item: items.length ? at : -1 });
|
||||
}
|
||||
});
|
||||
while (cells.length < CELLS) cells.push(null);
|
||||
return cells.slice(0, CELLS);
|
||||
}
|
||||
|
||||
// the headroom: how many more requests fit, at the pace the session's
|
||||
// prompts grew
|
||||
function headroom(series, free) {
|
||||
const steps = [];
|
||||
for (let i = 1; i < series.length; i++) {
|
||||
const d = series[i].tokens - series[i - 1].tokens;
|
||||
if (d > 0 && series[i].tokens >= series[i - 1].tokens * 0.6) steps.push(d);
|
||||
}
|
||||
const g = median(steps);
|
||||
return g > 0 ? Math.floor(free / g) : null;
|
||||
}
|
||||
|
||||
// count words a number with the phrase for one, or for many
|
||||
const count = (n, one, many) => n === 1 ? t(one) : t(many, { n });
|
||||
|
||||
// spark draws a session's prompts, request by request, against its
|
||||
// window; the one the card tells of is marked, and a point clicked opens
|
||||
// its request
|
||||
function spark(series, at, window, onPoint) {
|
||||
const W = 240, H = 34, pad = 3;
|
||||
// scaled to the session's peak, so a session that never came near its
|
||||
// window still shows how it grew; the window's line shows once it is near
|
||||
const peak = Math.max(...series.map((p) => p.tokens), 1);
|
||||
const top = window && peak > window * 0.6 ? Math.max(window, peak) : peak * 1.2;
|
||||
const x = (i) => series.length < 2 ? W / 2 : pad + (i / (series.length - 1)) * (W - pad * 2);
|
||||
const y = (n) => H - pad - (n / top) * (H - pad * 2);
|
||||
const box = el("span", "ctx-spark");
|
||||
const s = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
s.setAttribute("viewBox", `0 0 ${W} ${H}`);
|
||||
s.setAttribute("preserveAspectRatio", "none");
|
||||
box.append(s);
|
||||
const ns = (tag, attrs) => {
|
||||
const e = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
||||
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
|
||||
s.append(e);
|
||||
return e;
|
||||
};
|
||||
if (window && window <= top) ns("line", { x1: 0, x2: W, y1: y(window), y2: y(window), class: "win" });
|
||||
const pts = series.map((p, i) => `${x(i).toFixed(1)},${y(p.tokens).toFixed(1)}`);
|
||||
if (series.length > 1) {
|
||||
ns("path", { d: `M${pts[0]} L${pts.slice(1).join(" L")} L${x(series.length - 1).toFixed(1)},${H} L${x(0).toFixed(1)},${H} Z`, class: "area" });
|
||||
ns("polyline", { points: pts.join(" "), class: "line" });
|
||||
}
|
||||
series.forEach((p, i) => {
|
||||
const on = p.id === at;
|
||||
// the marker is a dot over the stretched svg, so it stays round
|
||||
if (on) {
|
||||
const dot = el("span", "at");
|
||||
dot.style.left = (x(i) / W * 100).toFixed(2) + "%";
|
||||
dot.style.top = (y(p.tokens) / H * 100).toFixed(2) + "%";
|
||||
box.append(dot);
|
||||
}
|
||||
if (!onPoint) return;
|
||||
const hit = ns("rect", { x: x(i) - Math.max(2, W / series.length / 2), y: 0, width: Math.max(4, W / series.length), height: H, class: "hit" + (on ? " on" : "") });
|
||||
const tip = document.createElementNS("http://www.w3.org/2000/svg", "title");
|
||||
tip.textContent = t("Request {n}: {tokens} tokens", { n: i + 1, tokens: fmtK(p.tokens) });
|
||||
hit.append(tip);
|
||||
hit.addEventListener("click", (e) => { e.stopPropagation(); onPoint(p); });
|
||||
});
|
||||
return box;
|
||||
}
|
||||
|
||||
// ctxCard draws a route's prompt. opts: series (the session's prompts,
|
||||
// [{id, tokens, time}]), onPoint (a point of it clicked), cache ({read,
|
||||
// total}, when the route carries no usage), crumbs (the footer's path)
|
||||
function ctxCard(r, opts = {}) {
|
||||
const p = r.prompt;
|
||||
const card = el("div", "ctx-card");
|
||||
if (!p) return card;
|
||||
const window = p.window || 0;
|
||||
const fill = window ? p.tokens / window : 0;
|
||||
const live = !r.done;
|
||||
|
||||
// the head: what it is and how it was counted
|
||||
const head = el("div", "ctx-head");
|
||||
const title = el("span", "ctx-title", t("Context window"));
|
||||
const st = el("span", "ctx-state " + (live ? "live" : p.counted ? "counted" : "est"));
|
||||
st.append(el("i"), t(live ? "Live" : p.counted ? "Counted" : "Estimated"));
|
||||
st.title = t(live ? "The request is on its way: the prompt is estimated from what the agent sent"
|
||||
: p.counted ? "As many tokens as the vendor counted; the parts are measured from the request and scaled to it"
|
||||
: "Estimated from what the agent sent: the vendor didn't say how many tokens it read");
|
||||
head.append(title, el("span", "grow"));
|
||||
if (r.model) head.append(el("code", "ctx-model", r.model));
|
||||
head.append(st);
|
||||
|
||||
// how full, and the cache
|
||||
const top = el("div", "ctx-top");
|
||||
const used = el("div", "ctx-stat");
|
||||
const k = el("div", "ctx-k");
|
||||
k.append(el("span", "", t("Context used")));
|
||||
if (window) {
|
||||
const [tone, word] = health(fill);
|
||||
const h = el("span", "ctx-health " + tone, t(word));
|
||||
k.append(h);
|
||||
}
|
||||
const big = el("div", "ctx-big");
|
||||
big.append(el("b", "", fmtK(p.tokens)));
|
||||
big.append(el("span", "", window ? " / " + fmtK(window) + " " + t("tokens") : " " + t("tokens")));
|
||||
const sub = el("div", "ctx-sub");
|
||||
if (window) {
|
||||
const bits = [t("{pct} full", { pct: pct(fill) })];
|
||||
const turns = opts.series ? headroom(opts.series, Math.max(0, window - p.tokens)) : null;
|
||||
if (turns !== null) bits.push(turns >= 999 ? t("plenty of headroom") : t("~{n} requests of headroom", { n: turns }));
|
||||
else bits.push(t("{tokens} free", { tokens: fmtK(Math.max(0, window - p.tokens)) }));
|
||||
sub.textContent = bits.join(" · ");
|
||||
} else sub.textContent = t("the model's window isn't known");
|
||||
used.append(k, big, sub);
|
||||
top.append(used);
|
||||
|
||||
const cache = cacheOf(r) || opts.cache;
|
||||
if (cache && p.counted !== false) {
|
||||
const c = el("div", "ctx-stat ctx-cache");
|
||||
const hit = cache.read / cache.total;
|
||||
c.append(el("div", "ctx-k", t("Prompt cache")));
|
||||
const cb = el("div", "ctx-big");
|
||||
cb.append(el("b", "", pct(hit)), el("span", "", " " + t("hit")));
|
||||
const bar = el("div", "ctx-bar");
|
||||
const fillBar = el("i");
|
||||
fillBar.style.width = (hit * 100).toFixed(1) + "%";
|
||||
bar.append(fillBar);
|
||||
c.append(cb, bar, el("div", "ctx-sub", t("{cached} cached · {fresh} new", { cached: fmtK(cache.read), fresh: fmtK(cache.total - cache.read) })));
|
||||
top.append(c);
|
||||
}
|
||||
|
||||
// the grid
|
||||
const grid = el("div", "ctx-waffle");
|
||||
grid.setAttribute("role", "img");
|
||||
grid.setAttribute("aria-label", t("{used} of {window} tokens used", { used: fmtK(p.tokens), window: fmtK(window || p.tokens) }));
|
||||
const cells = cellsOf(p, window);
|
||||
const partOf = new Map(p.parts.map((x) => [x.kind, x]));
|
||||
cells.forEach((c, i) => {
|
||||
const e = el("i", c ? "k-" + c.kind : "free");
|
||||
if (c) e.dataset.it = c.item;
|
||||
if (!opts.still) e.style.setProperty("--d", Math.round(i * 1.4) + "ms");
|
||||
grid.append(e);
|
||||
});
|
||||
const tip = el("div", "ctx-tip");
|
||||
tip.hidden = true;
|
||||
const showTip = (target) => {
|
||||
const idx = [...grid.children].indexOf(target);
|
||||
const c = cells[idx];
|
||||
tip.replaceChildren();
|
||||
const th = el("div", "ctx-tip-head");
|
||||
if (c) {
|
||||
const part = partOf.get(c.kind);
|
||||
th.append(el("i", "dot k-" + c.kind), el("b", "", partName(c.kind)), el("span", "grow"),
|
||||
el("span", "", fmtK(part.tokens) + " · " + pct(part.tokens / Math.max(1, p.tokens))));
|
||||
tip.append(th);
|
||||
const items = [...(part.items || [])].sort((a, b) => b.tokens - a.tokens);
|
||||
const shown = items.slice(0, 4);
|
||||
const here = items[c.item];
|
||||
if (here && !shown.includes(here)) shown[3] = here;
|
||||
for (const it of shown) {
|
||||
const row = el("div", "ctx-tip-row" + (it === here ? " on" : ""));
|
||||
row.append(el("span", "n", itemName(it)), el("span", "v", fmtK(it.tokens)));
|
||||
tip.append(row);
|
||||
}
|
||||
if (items.length > shown.length) tip.append(el("div", "ctx-tip-more", t("{n} more", { n: items.length - shown.length })));
|
||||
grid.dataset.f = c.kind;
|
||||
} else {
|
||||
th.append(el("i", "dot free"), el("b", "", t("Free space")), el("span", "grow"),
|
||||
el("span", "", fmtK(Math.max(0, window - p.tokens))));
|
||||
tip.append(th);
|
||||
grid.dataset.f = "free";
|
||||
}
|
||||
tip.hidden = false;
|
||||
const b = target.getBoundingClientRect(), cr = card.getBoundingClientRect();
|
||||
const w = tip.offsetWidth, h = tip.offsetHeight;
|
||||
let left = b.left - cr.left + b.width / 2 - w / 2;
|
||||
left = Math.max(8, Math.min(cr.width - w - 8, left));
|
||||
let topY = b.top - cr.top - h - 8;
|
||||
if (topY < 4) topY = b.bottom - cr.top + 8;
|
||||
tip.style.left = left + "px";
|
||||
tip.style.top = topY + "px";
|
||||
};
|
||||
grid.addEventListener("pointerover", (e) => { if (e.target.parentElement === grid) showTip(e.target); });
|
||||
grid.addEventListener("pointerleave", () => { tip.hidden = true; delete grid.dataset.f; });
|
||||
|
||||
// the legend: a part hovered lights its cells
|
||||
const legend = el("div", "ctx-legend");
|
||||
for (const kind of PARTS) {
|
||||
const part = partOf.get(kind);
|
||||
if (!part || part.tokens <= 0) continue;
|
||||
const li = el("span", "ctx-leg");
|
||||
li.title = t(PART_NOTES[kind]);
|
||||
li.append(el("i", "dot k-" + kind), el("span", "n", partName(kind)), el("span", "v", fmtK(part.tokens)));
|
||||
li.onpointerenter = () => { grid.dataset.f = kind; };
|
||||
li.onpointerleave = () => { delete grid.dataset.f; };
|
||||
legend.append(li);
|
||||
}
|
||||
if (window && window > p.tokens) {
|
||||
const li = el("span", "ctx-leg");
|
||||
li.append(el("i", "dot free"), el("span", "n", t("Free space")), el("span", "v", fmtK(window - p.tokens)));
|
||||
li.onpointerenter = () => { grid.dataset.f = "free"; };
|
||||
li.onpointerleave = () => { delete grid.dataset.f; };
|
||||
legend.append(li);
|
||||
}
|
||||
|
||||
// the contents, part by part
|
||||
const contents = el("div", "ctx-contents");
|
||||
const ch = el("div", "ctx-contents-head");
|
||||
const list = el("div", "ctx-rows");
|
||||
const tabs = [["all", t("All")], ...PARTS.filter((k) => partOf.get(k)?.tokens > 0).map((k) => [k, partName(k)])];
|
||||
let tab = opts.tab && tabs.some(([id]) => id === opts.tab) ? opts.tab : "all";
|
||||
let more = false;
|
||||
const drawRows = () => {
|
||||
let rows;
|
||||
if (tab === "all") {
|
||||
rows = p.parts.flatMap((x) => (x.items || []).map((it, i) => ({ it, i, kind: x.kind })));
|
||||
} else {
|
||||
const x = partOf.get(tab);
|
||||
rows = (x?.items || []).map((it, i) => ({ it, i, kind: tab }));
|
||||
}
|
||||
rows.sort((a, b) => b.it.tokens - a.it.tokens);
|
||||
const most = Math.max(1, ...rows.map((x) => x.it.tokens));
|
||||
const n = more ? rows.length : Math.min(rows.length, 8);
|
||||
const out = rows.slice(0, n).map(({ it, i, kind }) => {
|
||||
const row = el("div", "ctx-row");
|
||||
const tg = tagOf(it);
|
||||
const bar = el("span", "ctx-mini");
|
||||
const b = el("i", "k-" + kind);
|
||||
b.style.width = Math.max(2, (it.tokens / most) * 100).toFixed(1) + "%";
|
||||
bar.append(b);
|
||||
row.append(el("i", "dot k-" + kind), itemLabel(it, kind));
|
||||
if (it.n > 1 && it.tag !== "turns" && it.tag !== "more") row.append(el("span", "ctx-x", "×" + it.n));
|
||||
if (tg) row.append(el("code", "ctx-tag", tg));
|
||||
row.append(bar, el("span", "ctx-v", fmtK(it.tokens)));
|
||||
// its cells light up
|
||||
row.onpointerenter = () => {
|
||||
grid.dataset.f = "item";
|
||||
for (const [j, c] of cells.entries()) grid.children[j].classList.toggle("on", !!c && c.kind === kind && c.item === i);
|
||||
};
|
||||
row.onpointerleave = () => {
|
||||
delete grid.dataset.f;
|
||||
for (const e of grid.querySelectorAll("i.on")) e.classList.remove("on");
|
||||
};
|
||||
return row;
|
||||
});
|
||||
if (!rows.length) out.push(el("div", "ctx-empty", t("Nothing in it")));
|
||||
if (rows.length > 8) {
|
||||
const b = el("button", "text ctx-more", more ? t("Show fewer") : t("Show all {n}", { n: rows.length }));
|
||||
b.type = "button";
|
||||
b.onclick = () => { more = !more; drawRows(); };
|
||||
out.push(b);
|
||||
}
|
||||
list.replaceChildren(...out);
|
||||
};
|
||||
ch.append(el("span", "ctx-k", t("Contents")), el("span", "grow"),
|
||||
segs(tabs, tab, (id) => { tab = id; more = false; opts.onTab?.(id); drawRows(); }));
|
||||
contents.append(ch, list);
|
||||
drawRows();
|
||||
|
||||
// the foot: where it is, and the session's line
|
||||
const foot = el("div", "ctx-foot");
|
||||
const crumbs = el("code", "ctx-crumbs");
|
||||
crumbs.textContent = (opts.crumbs || [agentLabel(r.agent)]).filter(Boolean).join(" › ");
|
||||
const notes = [];
|
||||
if (p.held) notes.push(t("Earlier turns are held by the vendor"));
|
||||
if (p.turns) notes.push(count(p.turns, "1 turn", "{n} turns"));
|
||||
foot.append(crumbs, el("span", "grow"));
|
||||
if (notes.length) foot.append(el("span", "ctx-note", notes.join(" · ")));
|
||||
card.append(head, top, grid, legend, contents, foot);
|
||||
if (opts.series && opts.series.length > 1) {
|
||||
const sp = el("div", "ctx-session");
|
||||
sp.append(el("span", "ctx-k", t("This session")), spark(opts.series, r.id, window, opts.onPoint),
|
||||
el("span", "ctx-sub", count(opts.series.length, "1 request", "{n} requests")));
|
||||
card.append(sp);
|
||||
}
|
||||
card.append(tip);
|
||||
return card;
|
||||
}
|
||||
window.ctxCard = ctxCard;
|
||||
window.ctxFmt = fmtK;
|
||||
|
||||
// ---------- the Usage page's Context tab ----------
|
||||
|
||||
const CTX_RANGES = [["1", "Today"], ["7", "7 days"], ["30", "30 days"]];
|
||||
let ctxDays = "7";
|
||||
try { const d = localStorage.getItem("magpie.ctxDays"); if (CTX_RANGES.some(([id]) => id === d)) ctxDays = d; } catch {}
|
||||
let ctxData = null, ctxJSON = "", ctxRead = 0, ctxOpen = "", ctxAgent = "all";
|
||||
|
||||
const SCORE_NAMES = { cache: "Cache", lean: "Lean start", pace: "Pace", reliable: "Reliability" };
|
||||
const SCORE_NOTES = {
|
||||
cache: "How much of its prompts the vendor read from its cache: full marks at 90%",
|
||||
lean: "What every request starts with — system prompt, tools, memory — against the window: full marks at 5% or less",
|
||||
pace: "How much a request adds to the one before it: full marks at 1% of the window or less",
|
||||
reliable: "Its requests that didn't fail: none at 20% failing",
|
||||
};
|
||||
const TAGS = {
|
||||
"cache-friendly": ["Cache-friendly", (v) => t("{pct} of its prompts were read from the vendor's cache", { pct: pct(v) })],
|
||||
"cache-misses": ["Cache misses", (v) => t("Only {pct} of its prompts were read from the cache: each request pays for most of its prompt again", { pct: pct(v) })],
|
||||
lean: ["Lean start", (v) => t("Every request starts with {tokens} of system prompt, tools and memory", { tokens: fmtK(v) })],
|
||||
"heavy-start": ["Heavy start", (v) => t("Every request starts with {tokens} of system prompt, tools and memory, before a word of yours", { tokens: fmtK(v) })],
|
||||
"mcp-heavy": ["MCP-heavy", (v) => t("MCP servers' tool definitions take {tokens} of every request", { tokens: fmtK(v) })],
|
||||
"tool-heavy": ["Tool-heavy", (v) => t("Tool results are {pct} of its prompts", { pct: pct(v) })],
|
||||
"file-reader": ["Reads files", (v) => t("Files it read are {pct} of its prompts", { pct: pct(v) })],
|
||||
"memory-heavy": ["Memory-heavy", (v) => t("Instruction files and memory are {pct} of its prompts", { pct: pct(v) })],
|
||||
"fills-fast": ["Fills fast", (v) => t("A request adds {tokens} to the one before it, the median", { tokens: fmtK(v) })],
|
||||
"near-full": ["Runs near full", (v) => t("A session reached {pct} of its window", { pct: pct(v) })],
|
||||
compacts: ["Compacts", (v) => t("Its context was compacted {n} times", { n: v })],
|
||||
steady: ["Steady", () => t("Fewer than 1% of its requests failed")],
|
||||
flaky: ["Flaky", (v) => t("{pct} of its requests failed", { pct: pct(v) })],
|
||||
};
|
||||
const grade = (s) => s >= 85 ? ["great", "Excellent"] : s >= 70 ? ["good", "Good"] : s >= 50 ? ["fair", "Fair"] : ["poor", "Needs care"];
|
||||
|
||||
function ring(score) {
|
||||
const r = 26, c = 2 * Math.PI * r;
|
||||
const [tone] = grade(score);
|
||||
const box = el("div", "ctx-ring " + tone);
|
||||
const s = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
s.setAttribute("viewBox", "0 0 64 64");
|
||||
s.innerHTML = `<circle cx="32" cy="32" r="${r}" class="track"/><circle cx="32" cy="32" r="${r}" class="arc" stroke-dasharray="${c.toFixed(2)}" stroke-dashoffset="${c.toFixed(2)}" style="--to:${(c * (1 - score / 100)).toFixed(2)}"/>`;
|
||||
box.append(s, el("b", "", String(score)));
|
||||
return box;
|
||||
}
|
||||
|
||||
function shareBar(shares) {
|
||||
const bar = el("div", "ctx-share");
|
||||
for (const k of PARTS) {
|
||||
const v = shares?.[k] || 0;
|
||||
if (v <= 0.002) continue;
|
||||
const i = el("i", "k-" + k);
|
||||
i.style.flexGrow = v;
|
||||
i.title = partName(k) + " · " + pct(v);
|
||||
bar.append(i);
|
||||
}
|
||||
return bar;
|
||||
}
|
||||
|
||||
function agentCard(a) {
|
||||
const card = el("div", "ctx-agent");
|
||||
const head = el("div", "ctx-agent-head");
|
||||
const who = el("div", "ctx-who");
|
||||
who.append(agentIcon(a.agent), el("b", "", agentLabel(a.agent)));
|
||||
const meta = el("div", "ctx-who-sub", [count(a.requests, "1 request", "{n} requests"), count(a.sessions, "1 session", "{n} sessions")].join(" · "));
|
||||
const name = el("div", "ctx-who-box");
|
||||
name.append(who, meta);
|
||||
const [tone, word] = grade(a.score);
|
||||
const g = el("div", "ctx-grade");
|
||||
g.append(ring(a.score), el("span", "ctx-grade-word " + tone, t(word)));
|
||||
head.append(name, el("span", "grow"), g);
|
||||
|
||||
const scores = el("div", "ctx-scores");
|
||||
for (const s of a.scores || []) {
|
||||
const r = s.most ? s.points / s.most : 0;
|
||||
const row = el("div", "ctx-score " + (r >= 0.8 ? "ok" : r >= 0.5 ? "warn" : "bad"));
|
||||
row.title = t(SCORE_NOTES[s.key] || "");
|
||||
const bar = el("span", "ctx-bar");
|
||||
const f = el("i");
|
||||
f.style.width = ((s.points / s.most) * 100).toFixed(1) + "%";
|
||||
bar.append(f);
|
||||
row.append(el("span", "n", t(SCORE_NAMES[s.key] || s.key)), bar, el("span", "v", `${s.points}/${s.most}`));
|
||||
scores.append(row);
|
||||
}
|
||||
|
||||
const tags = el("div", "ctx-tags");
|
||||
for (const tg of a.tags || []) {
|
||||
const def = TAGS[tg.key];
|
||||
const chip = el("span", "ctx-chip " + tg.tone);
|
||||
chip.append(el("i"), def ? t(def[0]) : tg.key);
|
||||
chip.dataset.tt = def ? def[1](tg.value || 0) : tg.key;
|
||||
tags.append(chip);
|
||||
}
|
||||
|
||||
const facts = el("div", "ctx-facts");
|
||||
const fact = (label, value, note) => {
|
||||
const f = el("div", "ctx-fact");
|
||||
f.append(el("span", "ctx-k", t(label)), el("b", "", value));
|
||||
if (note) f.dataset.tt = t(note);
|
||||
facts.append(f);
|
||||
};
|
||||
fact("Typical prompt", fmtK(a.median), "The median request's prompt");
|
||||
fact("Largest", fmtK(a.p90), "90% of its requests' prompts are this size or smaller");
|
||||
fact("Starts with", fmtK(a.baseline), "System prompt, tools and memory: what every request carries before the conversation");
|
||||
fact("Grows by", a.growth ? "+" + fmtK(a.growth) : "—", "How much a request's prompt adds to the one before it, the median");
|
||||
fact("Cache hit", a.scores?.find((s) => s.key === "cache") && a.cache ? pct(a.cache) : "—", "Of its prompts, what the vendor read from its cache");
|
||||
fact("Window size", a.window ? fmtK(a.window) : "—", "The largest context window of the models it used");
|
||||
|
||||
const mix = el("div", "ctx-mix");
|
||||
mix.append(el("span", "ctx-k", t("What its prompts hold")), shareBar(a.shares));
|
||||
const keys = el("div", "ctx-mix-keys");
|
||||
for (const k of PARTS) {
|
||||
const v = a.shares?.[k] || 0;
|
||||
if (v < 0.01) continue;
|
||||
const s = el("span", "");
|
||||
s.append(el("i", "dot k-" + k), partName(k) + " " + pct(v));
|
||||
keys.append(s);
|
||||
}
|
||||
mix.append(keys);
|
||||
|
||||
const foot = el("div", "ctx-agent-foot");
|
||||
const models = el("span", "ctx-models");
|
||||
// by the model's own name: "live/claude/claude-sonnet-4-5" cut short
|
||||
// read the same for each of its models; the whole id in its tooltip
|
||||
for (const m of a.models || []) {
|
||||
const c = el("code", "", m.split("/").pop());
|
||||
c.title = m;
|
||||
models.append(c);
|
||||
}
|
||||
foot.append(models, el("span", "grow"));
|
||||
if (a.latestId) {
|
||||
const go = el("button", "text", t("Latest request"));
|
||||
go.type = "button";
|
||||
go.onclick = () => window.openRoute(a.latestId, a.latestTime).catch((e) => status(e.message, "err"));
|
||||
foot.append(go);
|
||||
}
|
||||
card.append(head, scores, tags, facts, mix, foot);
|
||||
return card;
|
||||
}
|
||||
|
||||
function sessionRow(s) {
|
||||
const open = ctxOpen === s.agent + "\x00" + s.key;
|
||||
const box = el("div", "ctx-sess" + (open ? " open" : ""));
|
||||
const row = el("button", "ctx-sess-row");
|
||||
row.type = "button";
|
||||
row.setAttribute("aria-expanded", String(open));
|
||||
const name = el("span", "ctx-sess-name");
|
||||
// an untitled session goes by the head of its id, the way the footer names it
|
||||
name.append(s.title ? el("b", "", s.title) : el("b", "raw", s.key.slice(0, 8)));
|
||||
name.append(el("code", "", s.model));
|
||||
const fill = s.window ? s.peak / s.window : 0;
|
||||
const [tone] = health(fill);
|
||||
const peak = el("span", "ctx-sess-peak " + tone, s.window ? pct(fill) : fmtK(s.peak));
|
||||
peak.dataset.tt = t("Its largest prompt: {tokens} of {window}", { tokens: fmtK(s.peak), window: fmtK(s.window) });
|
||||
const meta = el("span", "ctx-sess-meta", count(s.requests, "1 request", "{n} requests") + (s.compacts ? " · " + count(s.compacts, "1 compaction", "{n} compactions") : ""));
|
||||
row.append(agentIcon(s.agent), name, spark(s.points, s.latestId, s.window), peak, meta, el("span", "ctx-sess-when", ago(s.last)));
|
||||
row.onclick = () => {
|
||||
ctxOpen = open ? "" : s.agent + "\x00" + s.key;
|
||||
renderContext();
|
||||
};
|
||||
box.append(row);
|
||||
if (open && s.latest) {
|
||||
const last = s.points[s.points.length - 1];
|
||||
const r = { id: s.latestId, time: s.last, done: true, agent: s.agent, model: s.model, prompt: s.latest };
|
||||
const cache = last && s.latest.counted && last.tokens ? { read: last.cache || 0, total: last.tokens } : null;
|
||||
box.append(ctxCard(r, {
|
||||
still: true, cache,
|
||||
series: s.points,
|
||||
crumbs: [agentLabel(s.agent), s.title || s.key.slice(0, 12), t("latest request")],
|
||||
onPoint: (pt) => window.openRoute(pt.id, pt.time).catch((e) => status(e.message, "err")),
|
||||
}));
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
function renderContext() {
|
||||
const pane = $("#contextPane");
|
||||
if (!pane) return;
|
||||
const tools = el("div", "sess-tools ctx-tools");
|
||||
tools.append(segs(CTX_RANGES.map(([id, n]) => [id, t(n)]), ctxDays, (id) => {
|
||||
ctxDays = id;
|
||||
try { localStorage.setItem("magpie.ctxDays", id); } catch {}
|
||||
loadContext();
|
||||
}));
|
||||
tools.append(el("span", "grow"));
|
||||
pane.replaceChildren(tools);
|
||||
if (!ctxData) {
|
||||
pane.append(el("p", "usage-note", t("Reading the routing history…")));
|
||||
return;
|
||||
}
|
||||
pane.append(el("p", "usage-note", t("What each agent's prompts hold, as magpie's gateway read them: the score and tags are worked out from the requests of the range.")));
|
||||
if (!ctxData.agents.length) {
|
||||
const empty = el("div", "ctx-none");
|
||||
empty.append(el("b", "", t("No prompts read yet")), el("span", "", t("Requests your agents send through magpie show here.")));
|
||||
pane.append(empty);
|
||||
return;
|
||||
}
|
||||
const grid = el("div", "ctx-agents");
|
||||
for (const a of ctxData.agents) grid.append(agentCard(a));
|
||||
pane.append(grid);
|
||||
|
||||
const agents = [...new Set(ctxData.sessions.map((s) => s.agent))];
|
||||
const head = el("div", "row-head ctx-sess-head");
|
||||
head.append(el("span", "label", t("Sessions")), el("span", "grow"));
|
||||
if (agents.length > 1) {
|
||||
if (ctxAgent !== "all" && !agents.includes(ctxAgent)) ctxAgent = "all";
|
||||
head.append(segs([["all", t("All")], ...agents.map((id) => [id, agentLabel(id)])], ctxAgent, (id) => { ctxAgent = id; renderContext(); }));
|
||||
}
|
||||
pane.append(head);
|
||||
const list = el("div", "ctx-sess-list");
|
||||
const shown = ctxData.sessions.filter((s) => ctxAgent === "all" || s.agent === ctxAgent);
|
||||
for (const s of shown) list.append(sessionRow(s));
|
||||
if (!shown.length) list.append(el("p", "usage-note", t("No sessions in this range")));
|
||||
pane.append(list);
|
||||
}
|
||||
|
||||
async function loadContext() {
|
||||
const read = ++ctxRead, days = ctxDays;
|
||||
if (!ctxData || ctxData.days !== +days) { ctxData = null; renderContext(); }
|
||||
const data = await api("context?days=" + days);
|
||||
if (read !== ctxRead) return;
|
||||
// the auto refresh redraws only what changed: the same answer keeps the
|
||||
// pane (and what the pointer is on), and a new one comes in without its
|
||||
// entrance animations
|
||||
const json = JSON.stringify(data);
|
||||
if (ctxData && json === ctxJSON) return;
|
||||
const pane = $("#contextPane");
|
||||
if (pane) pane.classList.toggle("still", !!ctxData);
|
||||
ctxData = data, ctxJSON = json;
|
||||
renderContext();
|
||||
}
|
||||
window.loadContext = loadContext;
|
||||
window.renderContext = renderContext;
|
||||
})();
|
||||
@@ -4,6 +4,115 @@
|
||||
// works. {name} slots are filled by t(str, vars).
|
||||
const I18N = {
|
||||
zh: {
|
||||
// the context window (context.js)
|
||||
"1 request": "1 个请求",
|
||||
"1 session": "1 个会话",
|
||||
"1 compaction": "压缩 1 次",
|
||||
"1 turn": "1 轮",
|
||||
"System prompt": "系统提示词",
|
||||
"Tools": "工具",
|
||||
"Files": "文件",
|
||||
"Tool results": "工具结果",
|
||||
"The agent's own instructions": "agent 自带的指令",
|
||||
"The definitions of the tools it can call": "它可调用的工具的定义",
|
||||
"Instruction files, memory and reminders": "指令文件、记忆和提醒",
|
||||
"Files it read, and images": "它读取的文件和图片",
|
||||
"What the other tools it called returned": "它调用的其他工具返回的内容",
|
||||
"What you and the model said, turn by turn": "你和模型的对话,逐轮",
|
||||
"Agent prompt": "agent 提示词",
|
||||
"Harness messages": "harness 消息",
|
||||
"Billing header": "计费头",
|
||||
"Environment notes": "环境信息",
|
||||
"System reminders": "系统提醒",
|
||||
"Compacted summary": "压缩后的摘要",
|
||||
"Document": "文档",
|
||||
"Turn {n}": "第 {n} 轮",
|
||||
"{n} earlier turns": "更早的 {n} 轮",
|
||||
"{n} more": "另外 {n} 项",
|
||||
"Healthy": "健康",
|
||||
"Filling up": "渐满",
|
||||
"Near full": "将满",
|
||||
"Request {n}: {tokens} tokens": "第 {n} 个请求:{tokens} token",
|
||||
"Counted": "已计数",
|
||||
"Estimated": "估算",
|
||||
"The request is on its way: the prompt is estimated from what the agent sent": "请求进行中:提示词按 agent 发送的内容估算",
|
||||
"As many tokens as the vendor counted; the parts are measured from the request and scaled to it": "总数是服务商计的 token 数;各部分按请求内容测量,再按比例对齐到总数",
|
||||
"Estimated from what the agent sent: the vendor didn't say how many tokens it read": "按 agent 发送的内容估算:服务商没有告知它读了多少 token",
|
||||
"Context used": "已用上下文",
|
||||
"{pct} full": "已用 {pct}",
|
||||
"plenty of headroom": "余量充足",
|
||||
"~{n} requests of headroom": "约还能容纳 {n} 个请求",
|
||||
"{tokens} free": "剩余 {tokens}",
|
||||
"the model's window isn't known": "模型的窗口大小未知",
|
||||
"Prompt cache": "提示词缓存",
|
||||
"hit": "命中",
|
||||
"{cached} cached · {fresh} new": "{cached} 来自缓存 · {fresh} 新增",
|
||||
"{used} of {window} tokens used": "已用 {used} / {window} token",
|
||||
"Free space": "剩余空间",
|
||||
"Nothing in it": "这里没有内容",
|
||||
"Show all {n}": "显示全部 {n} 项",
|
||||
"Contents": "内容",
|
||||
"Earlier turns are held by the vendor": "更早的轮次保存在服务商那里",
|
||||
"{n} turns": "{n} 轮",
|
||||
"This session": "本会话",
|
||||
"Lean start": "起步轻",
|
||||
"Pace": "增速",
|
||||
"Reliability": "可靠性",
|
||||
"How much of its prompts the vendor read from its cache: full marks at 90%": "服务商从缓存读取的提示词占比:90% 即满分",
|
||||
"What every request starts with — system prompt, tools, memory — against the window: full marks at 5% or less": "每个请求一开始就带着的内容(系统提示词、工具、记忆)占窗口的比例:不超过 5% 即满分",
|
||||
"How much a request adds to the one before it: full marks at 1% of the window or less": "每个请求比上一个多出的量:不超过窗口的 1% 即满分",
|
||||
"Its requests that didn't fail: none at 20% failing": "未失败的请求:失败率达 20% 时为 0 分",
|
||||
"Cache-friendly": "缓存友好",
|
||||
"{pct} of its prompts were read from the vendor's cache": "它的提示词有 {pct} 是服务商从缓存读取的",
|
||||
"Cache misses": "缓存常未命中",
|
||||
"Only {pct} of its prompts were read from the cache: each request pays for most of its prompt again": "只有 {pct} 的提示词来自缓存:每个请求都要为大部分提示词重新付费",
|
||||
"Every request starts with {tokens} of system prompt, tools and memory": "每个请求一开始就带着 {tokens} 的系统提示词、工具和记忆",
|
||||
"Heavy start": "起步重",
|
||||
"Every request starts with {tokens} of system prompt, tools and memory, before a word of yours": "你还没说一个字,每个请求就已带着 {tokens} 的系统提示词、工具和记忆",
|
||||
"MCP-heavy": "MCP 偏重",
|
||||
"MCP servers' tool definitions take {tokens} of every request": "MCP 服务器的工具定义在每个请求里占 {tokens}",
|
||||
"Tool-heavy": "工具调用多",
|
||||
"Tool results are {pct} of its prompts": "工具结果占它提示词的 {pct}",
|
||||
"Reads files": "读文件多",
|
||||
"Files it read are {pct} of its prompts": "它读取的文件占提示词的 {pct}",
|
||||
"Memory-heavy": "记忆偏重",
|
||||
"Instruction files and memory are {pct} of its prompts": "指令文件和记忆占它提示词的 {pct}",
|
||||
"Fills fast": "涨得快",
|
||||
"A request adds {tokens} to the one before it, the median": "每个请求比上一个多 {tokens}(中位数)",
|
||||
"Runs near full": "常接近满",
|
||||
"A session reached {pct} of its window": "有会话用到了窗口的 {pct}",
|
||||
"Compacts": "会压缩",
|
||||
"Its context was compacted {n} times": "它的上下文被压缩了 {n} 次",
|
||||
"Steady": "稳定",
|
||||
"Fewer than 1% of its requests failed": "失败的请求不到 1%",
|
||||
"Flaky": "不稳定",
|
||||
"{pct} of its requests failed": "{pct} 的请求失败了",
|
||||
"Excellent": "优秀",
|
||||
"Good": "良好",
|
||||
"Fair": "一般",
|
||||
"Needs care": "待改进",
|
||||
"Typical prompt": "典型提示词",
|
||||
"The median request's prompt": "请求提示词的中位数",
|
||||
"Largest": "最大",
|
||||
"90% of its requests' prompts are this size or smaller": "90% 的请求提示词不超过这个大小",
|
||||
"Starts with": "起步",
|
||||
"System prompt, tools and memory: what every request carries before the conversation": "系统提示词、工具和记忆:每个请求在对话之前就带着的内容",
|
||||
"Grows by": "每次增长",
|
||||
"How much a request's prompt adds to the one before it, the median": "请求提示词比上一个多出的量(中位数)",
|
||||
"Cache hit": "缓存命中",
|
||||
"Of its prompts, what the vendor read from its cache": "它的提示词中服务商从缓存读取的比例",
|
||||
"Window size": "窗口大小",
|
||||
"The largest context window of the models it used": "它所用模型中最大的上下文窗口",
|
||||
"What its prompts hold": "提示词构成",
|
||||
"Latest request": "最新请求",
|
||||
"latest request": "最新请求",
|
||||
"Its largest prompt: {tokens} of {window}": "最大的提示词:{tokens} / {window}",
|
||||
"{n} compactions": "压缩 {n} 次",
|
||||
"Reading the routing history…": "正在读取路由历史…",
|
||||
"What each agent's prompts hold, as magpie's gateway read them: the score and tags are worked out from the requests of the range.": "每个 agent 的提示词里装了什么,由 magpie 网关读取;评分和标签根据所选时间范围内的请求算出。",
|
||||
"No prompts read yet": "还没有读到提示词",
|
||||
"Requests your agents send through magpie show here.": "你的 agent 经 magpie 发出的请求会显示在这里。",
|
||||
"No sessions in this range": "这个时间范围内没有会话",
|
||||
"No installed fonts found": "未找到已安装字体",
|
||||
"Interface font": "界面字体",
|
||||
"Code font": "代码字体",
|
||||
@@ -3617,6 +3726,115 @@ const I18N = {
|
||||
"credits": "积分",
|
||||
},
|
||||
"zh-TW": {
|
||||
// the context window (context.js)
|
||||
"1 request": "1 個請求",
|
||||
"1 session": "1 個會話",
|
||||
"1 compaction": "壓縮 1 次",
|
||||
"1 turn": "1 輪",
|
||||
"System prompt": "系統提示詞",
|
||||
"Tools": "工具",
|
||||
"Files": "檔案",
|
||||
"Tool results": "工具結果",
|
||||
"The agent's own instructions": "agent 自帶的指令",
|
||||
"The definitions of the tools it can call": "它可呼叫的工具的定義",
|
||||
"Instruction files, memory and reminders": "指令檔案、記憶和提醒",
|
||||
"Files it read, and images": "它讀取的檔案和圖片",
|
||||
"What the other tools it called returned": "它呼叫的其他工具傳回的內容",
|
||||
"What you and the model said, turn by turn": "你和模型的對話,逐輪",
|
||||
"Agent prompt": "agent 提示詞",
|
||||
"Harness messages": "harness 訊息",
|
||||
"Billing header": "計費標頭",
|
||||
"Environment notes": "環境資訊",
|
||||
"System reminders": "系統提醒",
|
||||
"Compacted summary": "壓縮後的摘要",
|
||||
"Document": "文件",
|
||||
"Turn {n}": "第 {n} 輪",
|
||||
"{n} earlier turns": "更早的 {n} 輪",
|
||||
"{n} more": "另外 {n} 項",
|
||||
"Healthy": "健康",
|
||||
"Filling up": "漸滿",
|
||||
"Near full": "將滿",
|
||||
"Request {n}: {tokens} tokens": "第 {n} 個請求:{tokens} token",
|
||||
"Counted": "已計數",
|
||||
"Estimated": "預估",
|
||||
"The request is on its way: the prompt is estimated from what the agent sent": "請求進行中:提示詞依 agent 送出的內容預估",
|
||||
"As many tokens as the vendor counted; the parts are measured from the request and scaled to it": "總數是服務商計的 token 數;各部分依請求內容測量,再按比例對齊到總數",
|
||||
"Estimated from what the agent sent: the vendor didn't say how many tokens it read": "依 agent 送出的內容預估:服務商沒有告知它讀了多少 token",
|
||||
"Context used": "已用上下文",
|
||||
"{pct} full": "已用 {pct}",
|
||||
"plenty of headroom": "餘裕充足",
|
||||
"~{n} requests of headroom": "約還能容納 {n} 個請求",
|
||||
"{tokens} free": "剩餘 {tokens}",
|
||||
"the model's window isn't known": "模型的視窗大小未知",
|
||||
"Prompt cache": "提示詞快取",
|
||||
"hit": "命中",
|
||||
"{cached} cached · {fresh} new": "{cached} 來自快取 · {fresh} 新增",
|
||||
"{used} of {window} tokens used": "已用 {used} / {window} token",
|
||||
"Free space": "剩餘空間",
|
||||
"Nothing in it": "這裡沒有內容",
|
||||
"Show all {n}": "顯示全部 {n} 項",
|
||||
"Contents": "內容",
|
||||
"Earlier turns are held by the vendor": "更早的輪次保存在服務商那裡",
|
||||
"{n} turns": "{n} 輪",
|
||||
"This session": "本會話",
|
||||
"Lean start": "起步輕",
|
||||
"Pace": "增速",
|
||||
"Reliability": "可靠性",
|
||||
"How much of its prompts the vendor read from its cache: full marks at 90%": "服務商從快取讀取的提示詞占比:90% 即滿分",
|
||||
"What every request starts with — system prompt, tools, memory — against the window: full marks at 5% or less": "每個請求一開始就帶著的內容(系統提示詞、工具、記憶)占視窗的比例:不超過 5% 即滿分",
|
||||
"How much a request adds to the one before it: full marks at 1% of the window or less": "每個請求比上一個多出的量:不超過視窗的 1% 即滿分",
|
||||
"Its requests that didn't fail: none at 20% failing": "未失敗的請求:失敗率達 20% 時為 0 分",
|
||||
"Cache-friendly": "快取友善",
|
||||
"{pct} of its prompts were read from the vendor's cache": "它的提示詞有 {pct} 是服務商從快取讀取的",
|
||||
"Cache misses": "快取常未命中",
|
||||
"Only {pct} of its prompts were read from the cache: each request pays for most of its prompt again": "只有 {pct} 的提示詞來自快取:每個請求都要為大部分提示詞重新付費",
|
||||
"Every request starts with {tokens} of system prompt, tools and memory": "每個請求一開始就帶著 {tokens} 的系統提示詞、工具和記憶",
|
||||
"Heavy start": "起步重",
|
||||
"Every request starts with {tokens} of system prompt, tools and memory, before a word of yours": "你還沒說一個字,每個請求就已帶著 {tokens} 的系統提示詞、工具和記憶",
|
||||
"MCP-heavy": "MCP 偏重",
|
||||
"MCP servers' tool definitions take {tokens} of every request": "MCP 伺服器的工具定義在每個請求裡占 {tokens}",
|
||||
"Tool-heavy": "工具呼叫多",
|
||||
"Tool results are {pct} of its prompts": "工具結果占它提示詞的 {pct}",
|
||||
"Reads files": "讀檔案多",
|
||||
"Files it read are {pct} of its prompts": "它讀取的檔案占提示詞的 {pct}",
|
||||
"Memory-heavy": "記憶偏重",
|
||||
"Instruction files and memory are {pct} of its prompts": "指令檔案和記憶占它提示詞的 {pct}",
|
||||
"Fills fast": "漲得快",
|
||||
"A request adds {tokens} to the one before it, the median": "每個請求比上一個多 {tokens}(中位數)",
|
||||
"Runs near full": "常接近滿",
|
||||
"A session reached {pct} of its window": "有會話用到了視窗的 {pct}",
|
||||
"Compacts": "會壓縮",
|
||||
"Its context was compacted {n} times": "它的上下文被壓縮了 {n} 次",
|
||||
"Steady": "穩定",
|
||||
"Fewer than 1% of its requests failed": "失敗的請求不到 1%",
|
||||
"Flaky": "不穩定",
|
||||
"{pct} of its requests failed": "{pct} 的請求失敗了",
|
||||
"Excellent": "優秀",
|
||||
"Good": "良好",
|
||||
"Fair": "普通",
|
||||
"Needs care": "待改進",
|
||||
"Typical prompt": "典型提示詞",
|
||||
"The median request's prompt": "請求提示詞的中位數",
|
||||
"Largest": "最大",
|
||||
"90% of its requests' prompts are this size or smaller": "90% 的請求提示詞不超過這個大小",
|
||||
"Starts with": "起步",
|
||||
"System prompt, tools and memory: what every request carries before the conversation": "系統提示詞、工具和記憶:每個請求在對話之前就帶著的內容",
|
||||
"Grows by": "每次增長",
|
||||
"How much a request's prompt adds to the one before it, the median": "請求提示詞比上一個多出的量(中位數)",
|
||||
"Cache hit": "快取命中",
|
||||
"Of its prompts, what the vendor read from its cache": "它的提示詞中服務商從快取讀取的比例",
|
||||
"Window size": "視窗大小",
|
||||
"The largest context window of the models it used": "它所用模型中最大的上下文視窗",
|
||||
"What its prompts hold": "提示詞構成",
|
||||
"Latest request": "最新請求",
|
||||
"latest request": "最新請求",
|
||||
"Its largest prompt: {tokens} of {window}": "最大的提示詞:{tokens} / {window}",
|
||||
"{n} compactions": "壓縮 {n} 次",
|
||||
"Reading the routing history…": "正在讀取路由紀錄…",
|
||||
"What each agent's prompts hold, as magpie's gateway read them: the score and tags are worked out from the requests of the range.": "每個 agent 的提示詞裡裝了什麼,由 magpie 閘道讀取;評分和標籤根據所選時間範圍內的請求算出。",
|
||||
"No prompts read yet": "還沒有讀到提示詞",
|
||||
"Requests your agents send through magpie show here.": "你的 agent 經 magpie 送出的請求會顯示在這裡。",
|
||||
"No sessions in this range": "這個時間範圍內沒有會話",
|
||||
"Route by what it asks for": "按意圖分流",
|
||||
"It can pick the model too: add a rule with an intent": "它也能挑模型:新增一條帶意圖的規則",
|
||||
"already in the library: {names}": "資源庫裡已有:{names}",
|
||||
@@ -7213,6 +7431,115 @@ const I18N = {
|
||||
"credits": "積分",
|
||||
},
|
||||
ja: {
|
||||
// the context window (context.js)
|
||||
"1 request": "1 件のリクエスト",
|
||||
"1 session": "1 件のセッション",
|
||||
"1 compaction": "圧縮 1 回",
|
||||
"1 turn": "1 ターン",
|
||||
"System prompt": "システムプロンプト",
|
||||
"Tools": "ツール",
|
||||
"Files": "ファイル",
|
||||
"Tool results": "ツールの結果",
|
||||
"The agent's own instructions": "エージェント自身の指示",
|
||||
"The definitions of the tools it can call": "呼び出せるツールの定義",
|
||||
"Instruction files, memory and reminders": "指示ファイル、メモリ、リマインダー",
|
||||
"Files it read, and images": "読み込んだファイルと画像",
|
||||
"What the other tools it called returned": "呼び出したその他のツールが返した内容",
|
||||
"What you and the model said, turn by turn": "あなたとモデルのやり取り(ターンごと)",
|
||||
"Agent prompt": "エージェントのプロンプト",
|
||||
"Harness messages": "ハーネスのメッセージ",
|
||||
"Billing header": "課金ヘッダー",
|
||||
"Environment notes": "環境情報",
|
||||
"System reminders": "システムリマインダー",
|
||||
"Compacted summary": "圧縮された要約",
|
||||
"Document": "ドキュメント",
|
||||
"Turn {n}": "ターン {n}",
|
||||
"{n} earlier turns": "それ以前の {n} ターン",
|
||||
"{n} more": "ほか {n} 件",
|
||||
"Healthy": "良好",
|
||||
"Filling up": "埋まりつつある",
|
||||
"Near full": "ほぼ満杯",
|
||||
"Request {n}: {tokens} tokens": "リクエスト {n}:{tokens} トークン",
|
||||
"Counted": "カウント済み",
|
||||
"Estimated": "推定",
|
||||
"The request is on its way: the prompt is estimated from what the agent sent": "リクエスト送信中:プロンプトはエージェントが送った内容から推定しています",
|
||||
"As many tokens as the vendor counted; the parts are measured from the request and scaled to it": "トークン数はベンダーが数えたものです。各部分はリクエストから測り、その数に合わせて按分しています",
|
||||
"Estimated from what the agent sent: the vendor didn't say how many tokens it read": "エージェントが送った内容から推定:ベンダーは読み込んだトークン数を返しませんでした",
|
||||
"Context used": "使用中のコンテキスト",
|
||||
"{pct} full": "{pct} 使用",
|
||||
"plenty of headroom": "余裕あり",
|
||||
"~{n} requests of headroom": "あと約 {n} リクエスト分の余裕",
|
||||
"{tokens} free": "残り {tokens}",
|
||||
"the model's window isn't known": "モデルのウィンドウサイズは不明",
|
||||
"Prompt cache": "プロンプトキャッシュ",
|
||||
"hit": "ヒット",
|
||||
"{cached} cached · {fresh} new": "{cached} キャッシュ · {fresh} 新規",
|
||||
"{used} of {window} tokens used": "{window} トークン中 {used} 使用",
|
||||
"Free space": "空き",
|
||||
"Nothing in it": "中身はありません",
|
||||
"Show all {n}": "{n} 件すべて表示",
|
||||
"Contents": "内訳",
|
||||
"Earlier turns are held by the vendor": "以前のターンはベンダー側に保持されています",
|
||||
"{n} turns": "{n} ターン",
|
||||
"This session": "このセッション",
|
||||
"Lean start": "軽いスタート",
|
||||
"Pace": "増え方",
|
||||
"Reliability": "信頼性",
|
||||
"How much of its prompts the vendor read from its cache: full marks at 90%": "ベンダーがキャッシュから読んだプロンプトの割合:90% で満点",
|
||||
"What every request starts with — system prompt, tools, memory — against the window: full marks at 5% or less": "すべてのリクエストが最初から持つもの(システムプロンプト、ツール、メモリ)がウィンドウに占める割合:5% 以下で満点",
|
||||
"How much a request adds to the one before it: full marks at 1% of the window or less": "1 つのリクエストが前回より増える量:ウィンドウの 1% 以下で満点",
|
||||
"Its requests that didn't fail: none at 20% failing": "失敗しなかったリクエスト:失敗率 20% で 0 点",
|
||||
"Cache-friendly": "キャッシュ効率が良い",
|
||||
"{pct} of its prompts were read from the vendor's cache": "プロンプトの {pct} がベンダーのキャッシュから読まれました",
|
||||
"Cache misses": "キャッシュミスが多い",
|
||||
"Only {pct} of its prompts were read from the cache: each request pays for most of its prompt again": "キャッシュから読まれたのはプロンプトの {pct} だけ:毎回プロンプトの大半を払い直しています",
|
||||
"Every request starts with {tokens} of system prompt, tools and memory": "すべてのリクエストがシステムプロンプト、ツール、メモリの {tokens} から始まります",
|
||||
"Heavy start": "重いスタート",
|
||||
"Every request starts with {tokens} of system prompt, tools and memory, before a word of yours": "あなたが何か言う前から、すべてのリクエストがシステムプロンプト、ツール、メモリの {tokens} を抱えています",
|
||||
"MCP-heavy": "MCP が重い",
|
||||
"MCP servers' tool definitions take {tokens} of every request": "MCP サーバーのツール定義が毎回 {tokens} を占めています",
|
||||
"Tool-heavy": "ツール多用",
|
||||
"Tool results are {pct} of its prompts": "ツールの結果がプロンプトの {pct} を占めています",
|
||||
"Reads files": "ファイルをよく読む",
|
||||
"Files it read are {pct} of its prompts": "読み込んだファイルがプロンプトの {pct} を占めています",
|
||||
"Memory-heavy": "メモリが重い",
|
||||
"Instruction files and memory are {pct} of its prompts": "指示ファイルとメモリがプロンプトの {pct} を占めています",
|
||||
"Fills fast": "すぐ埋まる",
|
||||
"A request adds {tokens} to the one before it, the median": "1 リクエストごとに前回より {tokens} 増えます(中央値)",
|
||||
"Runs near full": "満杯近くまで使う",
|
||||
"A session reached {pct} of its window": "ウィンドウの {pct} に達したセッションがあります",
|
||||
"Compacts": "圧縮あり",
|
||||
"Its context was compacted {n} times": "コンテキストが {n} 回圧縮されました",
|
||||
"Steady": "安定",
|
||||
"Fewer than 1% of its requests failed": "失敗したリクエストは 1% 未満です",
|
||||
"Flaky": "不安定",
|
||||
"{pct} of its requests failed": "リクエストの {pct} が失敗しました",
|
||||
"Excellent": "優秀",
|
||||
"Good": "良い",
|
||||
"Fair": "まずまず",
|
||||
"Needs care": "要改善",
|
||||
"Typical prompt": "典型的なプロンプト",
|
||||
"The median request's prompt": "リクエストのプロンプトの中央値",
|
||||
"Largest": "最大",
|
||||
"90% of its requests' prompts are this size or smaller": "リクエストの 90% のプロンプトがこのサイズ以下です",
|
||||
"Starts with": "スタート時",
|
||||
"System prompt, tools and memory: what every request carries before the conversation": "システムプロンプト、ツール、メモリ:会話の前からすべてのリクエストが抱えるもの",
|
||||
"Grows by": "増加量",
|
||||
"How much a request's prompt adds to the one before it, the median": "リクエストのプロンプトが前回より増える量(中央値)",
|
||||
"Cache hit": "キャッシュヒット",
|
||||
"Of its prompts, what the vendor read from its cache": "プロンプトのうちベンダーがキャッシュから読んだ割合",
|
||||
"Window size": "ウィンドウサイズ",
|
||||
"The largest context window of the models it used": "使ったモデルの最大コンテキストウィンドウ",
|
||||
"What its prompts hold": "プロンプトの中身",
|
||||
"Latest request": "最新のリクエスト",
|
||||
"latest request": "最新のリクエスト",
|
||||
"Its largest prompt: {tokens} of {window}": "最大のプロンプト:{window} 中 {tokens}",
|
||||
"{n} compactions": "圧縮 {n} 回",
|
||||
"Reading the routing history…": "ルーティング履歴を読み込み中…",
|
||||
"What each agent's prompts hold, as magpie's gateway read them: the score and tags are worked out from the requests of the range.": "各エージェントのプロンプトの中身を magpie のゲートウェイが読み取ったものです。スコアとタグは期間内のリクエストから算出しています。",
|
||||
"No prompts read yet": "まだプロンプトを読み取っていません",
|
||||
"Requests your agents send through magpie show here.": "エージェントが magpie 経由で送ったリクエストがここに表示されます。",
|
||||
"No sessions in this range": "この期間にセッションはありません",
|
||||
"No installed fonts found": "インストール済みフォントが見つかりません",
|
||||
"Interface font": "インターフェースのフォント",
|
||||
"Code font": "コードのフォント",
|
||||
@@ -10811,6 +11138,115 @@ const I18N = {
|
||||
"Main navigation": "メインナビゲーション",
|
||||
},
|
||||
de: {
|
||||
// the context window (context.js)
|
||||
"1 request": "1 Anfrage",
|
||||
"1 session": "1 Sitzung",
|
||||
"1 compaction": "1 Komprimierung",
|
||||
"1 turn": "1 Runde",
|
||||
"System prompt": "Systemprompt",
|
||||
"Tools": "Tools",
|
||||
"Files": "Dateien",
|
||||
"Tool results": "Tool-Ergebnisse",
|
||||
"The agent's own instructions": "Die eigenen Anweisungen des Agents",
|
||||
"The definitions of the tools it can call": "Die Definitionen der Tools, die er aufrufen kann",
|
||||
"Instruction files, memory and reminders": "Anweisungsdateien, Memory und Erinnerungen",
|
||||
"Files it read, and images": "Gelesene Dateien und Bilder",
|
||||
"What the other tools it called returned": "Was die übrigen aufgerufenen Tools zurückgaben",
|
||||
"What you and the model said, turn by turn": "Was du und das Modell gesagt habt, Runde für Runde",
|
||||
"Agent prompt": "Agent-Prompt",
|
||||
"Harness messages": "Harness-Nachrichten",
|
||||
"Billing header": "Abrechnungs-Header",
|
||||
"Environment notes": "Umgebungsangaben",
|
||||
"System reminders": "Systemerinnerungen",
|
||||
"Compacted summary": "Komprimierte Zusammenfassung",
|
||||
"Document": "Dokument",
|
||||
"Turn {n}": "Runde {n}",
|
||||
"{n} earlier turns": "{n} frühere Runden",
|
||||
"{n} more": "{n} weitere",
|
||||
"Healthy": "Gesund",
|
||||
"Filling up": "Füllt sich",
|
||||
"Near full": "Fast voll",
|
||||
"Request {n}: {tokens} tokens": "Anfrage {n}: {tokens} Token",
|
||||
"Counted": "Gezählt",
|
||||
"Estimated": "Geschätzt",
|
||||
"The request is on its way: the prompt is estimated from what the agent sent": "Die Anfrage läuft noch: Der Prompt ist aus dem geschätzt, was der Agent gesendet hat",
|
||||
"As many tokens as the vendor counted; the parts are measured from the request and scaled to it": "So viele Token, wie der Anbieter gezählt hat; die Teile sind aus der Anfrage gemessen und darauf skaliert",
|
||||
"Estimated from what the agent sent: the vendor didn't say how many tokens it read": "Aus dem geschätzt, was der Agent gesendet hat: Der Anbieter hat nicht gesagt, wie viele Token er gelesen hat",
|
||||
"Context used": "Belegter Kontext",
|
||||
"{pct} full": "{pct} belegt",
|
||||
"plenty of headroom": "viel Spielraum",
|
||||
"~{n} requests of headroom": "Platz für ~{n} Anfragen",
|
||||
"{tokens} free": "{tokens} frei",
|
||||
"the model's window isn't known": "Das Fenster des Modells ist unbekannt",
|
||||
"Prompt cache": "Prompt-Cache",
|
||||
"hit": "Treffer",
|
||||
"{cached} cached · {fresh} new": "{cached} aus dem Cache · {fresh} neu",
|
||||
"{used} of {window} tokens used": "{used} von {window} Token belegt",
|
||||
"Free space": "Frei",
|
||||
"Nothing in it": "Nichts darin",
|
||||
"Show all {n}": "Alle {n} anzeigen",
|
||||
"Contents": "Inhalt",
|
||||
"Earlier turns are held by the vendor": "Frühere Runden hält der Anbieter",
|
||||
"{n} turns": "{n} Runden",
|
||||
"This session": "Diese Sitzung",
|
||||
"Lean start": "Schlanker Start",
|
||||
"Pace": "Tempo",
|
||||
"Reliability": "Zuverlässigkeit",
|
||||
"How much of its prompts the vendor read from its cache: full marks at 90%": "Wie viel seiner Prompts der Anbieter aus dem Cache las: volle Punktzahl ab 90 %",
|
||||
"What every request starts with — system prompt, tools, memory — against the window: full marks at 5% or less": "Womit jede Anfrage beginnt – Systemprompt, Tools, Memory – im Verhältnis zum Fenster: volle Punktzahl bis 5 %",
|
||||
"How much a request adds to the one before it: full marks at 1% of the window or less": "Wie viel eine Anfrage zur vorigen hinzufügt: volle Punktzahl bis 1 % des Fensters",
|
||||
"Its requests that didn't fail: none at 20% failing": "Seine Anfragen ohne Fehler: null Punkte bei 20 % Fehlern",
|
||||
"Cache-friendly": "Cache-freundlich",
|
||||
"{pct} of its prompts were read from the vendor's cache": "{pct} seiner Prompts las der Anbieter aus dem Cache",
|
||||
"Cache misses": "Cache-Fehlschläge",
|
||||
"Only {pct} of its prompts were read from the cache: each request pays for most of its prompt again": "Nur {pct} seiner Prompts kamen aus dem Cache: Jede Anfrage bezahlt den Großteil ihres Prompts erneut",
|
||||
"Every request starts with {tokens} of system prompt, tools and memory": "Jede Anfrage beginnt mit {tokens} an Systemprompt, Tools und Memory",
|
||||
"Heavy start": "Schwerer Start",
|
||||
"Every request starts with {tokens} of system prompt, tools and memory, before a word of yours": "Noch vor deinem ersten Wort beginnt jede Anfrage mit {tokens} an Systemprompt, Tools und Memory",
|
||||
"MCP-heavy": "MCP-lastig",
|
||||
"MCP servers' tool definitions take {tokens} of every request": "Die Tool-Definitionen der MCP-Server belegen {tokens} jeder Anfrage",
|
||||
"Tool-heavy": "Tool-lastig",
|
||||
"Tool results are {pct} of its prompts": "Tool-Ergebnisse machen {pct} seiner Prompts aus",
|
||||
"Reads files": "Liest Dateien",
|
||||
"Files it read are {pct} of its prompts": "Gelesene Dateien machen {pct} seiner Prompts aus",
|
||||
"Memory-heavy": "Memory-lastig",
|
||||
"Instruction files and memory are {pct} of its prompts": "Anweisungsdateien und Memory machen {pct} seiner Prompts aus",
|
||||
"Fills fast": "Füllt sich schnell",
|
||||
"A request adds {tokens} to the one before it, the median": "Eine Anfrage fügt der vorigen {tokens} hinzu (Median)",
|
||||
"Runs near full": "Läuft fast voll",
|
||||
"A session reached {pct} of its window": "Eine Sitzung erreichte {pct} ihres Fensters",
|
||||
"Compacts": "Komprimiert",
|
||||
"Its context was compacted {n} times": "Sein Kontext wurde {n}-mal komprimiert",
|
||||
"Steady": "Stabil",
|
||||
"Fewer than 1% of its requests failed": "Weniger als 1 % seiner Anfragen schlugen fehl",
|
||||
"Flaky": "Wackelig",
|
||||
"{pct} of its requests failed": "{pct} seiner Anfragen schlugen fehl",
|
||||
"Excellent": "Ausgezeichnet",
|
||||
"Good": "Gut",
|
||||
"Fair": "Mittel",
|
||||
"Needs care": "Ausbaufähig",
|
||||
"Typical prompt": "Typischer Prompt",
|
||||
"The median request's prompt": "Der Prompt der mittleren Anfrage (Median)",
|
||||
"Largest": "Größte",
|
||||
"90% of its requests' prompts are this size or smaller": "90 % seiner Prompts sind so groß oder kleiner",
|
||||
"Starts with": "Startet mit",
|
||||
"System prompt, tools and memory: what every request carries before the conversation": "Systemprompt, Tools und Memory: was jede Anfrage vor der Unterhaltung mitbringt",
|
||||
"Grows by": "Wächst um",
|
||||
"How much a request's prompt adds to the one before it, the median": "Wie viel der Prompt einer Anfrage zur vorigen hinzufügt (Median)",
|
||||
"Cache hit": "Cache-Treffer",
|
||||
"Of its prompts, what the vendor read from its cache": "Der Anteil seiner Prompts, den der Anbieter aus dem Cache las",
|
||||
"Window size": "Fenstergröße",
|
||||
"The largest context window of the models it used": "Das größte Kontextfenster der Modelle, die er nutzte",
|
||||
"What its prompts hold": "Was seine Prompts enthalten",
|
||||
"Latest request": "Neueste Anfrage",
|
||||
"latest request": "neueste Anfrage",
|
||||
"Its largest prompt: {tokens} of {window}": "Sein größter Prompt: {tokens} von {window}",
|
||||
"{n} compactions": "{n} Komprimierungen",
|
||||
"Reading the routing history…": "Routing-Verlauf wird gelesen …",
|
||||
"What each agent's prompts hold, as magpie's gateway read them: the score and tags are worked out from the requests of the range.": "Was die Prompts jedes Agents enthalten, wie magpies Gateway sie las: Bewertung und Tags ergeben sich aus den Anfragen des Zeitraums.",
|
||||
"No prompts read yet": "Noch keine Prompts gelesen",
|
||||
"Requests your agents send through magpie show here.": "Anfragen, die deine Agents über magpie senden, erscheinen hier.",
|
||||
"No sessions in this range": "Keine Sitzungen in diesem Zeitraum",
|
||||
"No installed fonts found": "Keine installierten Schriften gefunden",
|
||||
"Interface font": "Oberflächenschrift",
|
||||
"Code font": "Codeschrift",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<link rel="stylesheet" href="library.css">
|
||||
<link rel="stylesheet" href="plugins.css">
|
||||
<link rel="stylesheet" href="sessions.css">
|
||||
<link rel="stylesheet" href="context.css">
|
||||
<link rel="stylesheet" href="omarchy.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -192,6 +193,7 @@
|
||||
<div class="led-pager" id="ledPager" hidden></div>
|
||||
<p class="usage-note" id="ledNote"></p>
|
||||
</div>
|
||||
<div class="usage-pane ctx-pane" id="contextPane" hidden></div>
|
||||
<div class="usage-pane sess-pane" id="sessionsPane" hidden>
|
||||
<div class="sess-tools">
|
||||
<div class="segs" id="sessAgent"></div>
|
||||
@@ -501,5 +503,6 @@
|
||||
<script src="library.js"></script>
|
||||
<script src="plugins.js"></script>
|
||||
<script src="sessions.js"></script>
|
||||
<script src="context.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -70,7 +70,9 @@
|
||||
const log = el("div", "rt-log");
|
||||
const logHead = el("div", "rt-log-head");
|
||||
const steps = el("ol", "rt-steps");
|
||||
log.append(logHead, steps);
|
||||
// what the request's prompt held: its context window (context.js)
|
||||
const ctxBox = el("div", "rt-ctx");
|
||||
log.append(logHead, steps, ctxBox);
|
||||
const off = el("div", "none rt-off");
|
||||
// over the stage while a replay plays: the time it is replaying, which
|
||||
// requests are in flight then, and where it is among them
|
||||
@@ -1273,6 +1275,37 @@
|
||||
}
|
||||
}
|
||||
renderSteps(r);
|
||||
renderCtx(r);
|
||||
}
|
||||
// renderCtx draws the request's context window under its story, with
|
||||
// its session's prompts request by request; drawn again only when what
|
||||
// it shows changes, and its cells come in only for a request newly shown
|
||||
let ctxKey = "", ctxShown = 0, ctxTab = "all";
|
||||
function renderCtx(r) {
|
||||
if (!r.prompt || !window.ctxCard) {
|
||||
if (ctxKey) { ctxKey = ""; ctxBox.replaceChildren(); }
|
||||
return;
|
||||
}
|
||||
const sk = sessionKey(r);
|
||||
const same = (x) => x.prompt && x.agent === r.agent && !x.kind && (sk ? sessionKey(x) === sk : r.conv && x.conv === r.conv);
|
||||
const series = (sk || r.conv) && !r.kind ? listed().filter(same).sort((a, b) => a.id - b.id)
|
||||
.map((x) => ({ id: x.id, tokens: x.prompt.tokens, time: x.time })) : null;
|
||||
if (series && !series.some((x) => x.id === r.id)) series.push({ id: r.id, tokens: r.prompt.tokens, time: r.time });
|
||||
const key = JSON.stringify([document.documentElement.lang, r.id, r.done, r.prompt.tokens, r.prompt.counted, r.prompt.window, r.usage?.length, series?.map((x) => x.id + ":" + x.tokens).join()]);
|
||||
if (key === ctxKey) return;
|
||||
ctxKey = key;
|
||||
const still = ctxShown === r.id;
|
||||
ctxShown = r.id;
|
||||
const sess = groupSession(r) || r.conv || "";
|
||||
ctxBox.replaceChildren(window.ctxCard(r, {
|
||||
still, series, tab: ctxTab,
|
||||
onTab: (id) => { ctxTab = id; },
|
||||
crumbs: [agentName(r.agent), sess && (sess.length > 14 ? sess.slice(0, 12) + "…" : sess), "#" + r.id],
|
||||
onPoint: (pt) => {
|
||||
const x = routes.get(pt.id) || listed().find((y) => y.id === pt.id);
|
||||
if (x) pick(x);
|
||||
},
|
||||
}));
|
||||
}
|
||||
// logoed puts the provider's logo before the first account, key or
|
||||
// provider a line of the story names, so who it is about reads at a
|
||||
|
||||
@@ -98,7 +98,7 @@ func TestPageNamesItsFilesByContent(t *testing.T) {
|
||||
t.Errorf("/: Content-Type %q", ct)
|
||||
}
|
||||
body := page.Body.String()
|
||||
for _, f := range []string{"app.js", "routing.js", "library.js", "plugins.js", "sessions.js", "i18n.js", "compat.js", "app.css", "routing.css"} {
|
||||
for _, f := range []string{"app.js", "routing.js", "library.js", "plugins.js", "sessions.js", "context.js", "i18n.js", "compat.js", "app.css", "routing.css", "context.css"} {
|
||||
b, err := fs.ReadFile(staticFS(), f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
package gui
|
||||
|
||||
// The Context tab: each agent's context windows as the gateway saw them —
|
||||
// what its prompts hold, how fast they fill, how much its vendor read from
|
||||
// its cache — with a score and tags worked out from those, its sessions'
|
||||
// fill turn by turn, and each request's prompt one click away in Routing.
|
||||
// Read from the routing history (gateway.Route.Prompt), the latest
|
||||
// requests from the live trace.
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/gateway"
|
||||
"github.com/yetone/magpie/internal/sessions"
|
||||
)
|
||||
|
||||
// defaultWindow is the window a prompt is measured against when the
|
||||
// model's isn't known.
|
||||
const defaultWindow = 200000
|
||||
|
||||
type ctxPoint struct {
|
||||
ID int64 `json:"id"`
|
||||
Time time.Time `json:"time"`
|
||||
Tokens int `json:"tokens"`
|
||||
Window int `json:"window,omitempty"`
|
||||
Cache int `json:"cache,omitempty"` // of Tokens, read from the vendor's cache
|
||||
}
|
||||
|
||||
type ctxSession struct {
|
||||
Agent string `json:"agent"`
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Model string `json:"model"`
|
||||
First time.Time `json:"first"`
|
||||
Last time.Time `json:"last"`
|
||||
Requests int `json:"requests"`
|
||||
Peak int `json:"peak"`
|
||||
Window int `json:"window"`
|
||||
Compacts int `json:"compacts,omitempty"`
|
||||
Latest *gateway.Prompt `json:"latest"` // the newest request's prompt
|
||||
LatestID int64 `json:"latestId"`
|
||||
Points []ctxPoint `json:"points"` // its requests' prompts, oldest first
|
||||
}
|
||||
|
||||
// ctxScore is one of what makes an agent's score: its points of most.
|
||||
type ctxScore struct {
|
||||
Key string `json:"key"` // cache, lean, pace, reliable
|
||||
Points float64 `json:"points"`
|
||||
Most float64 `json:"most"`
|
||||
}
|
||||
|
||||
// ctxTag is something an agent's prompts tell of it.
|
||||
type ctxTag struct {
|
||||
Key string `json:"key"`
|
||||
Tone string `json:"tone"` // good, warn, info
|
||||
Value float64 `json:"value,omitempty"` // the number it is told by
|
||||
}
|
||||
|
||||
type ctxAgent struct {
|
||||
Agent string `json:"agent"`
|
||||
Requests int `json:"requests"` // with a prompt read
|
||||
Sessions int `json:"sessions"`
|
||||
Errors int `json:"errors"`
|
||||
Calls int `json:"calls"` // all of its requests, for the error rate
|
||||
Median int `json:"median"`
|
||||
P90 int `json:"p90"`
|
||||
Baseline int `json:"baseline"` // its system prompt, tools and memory: what every request starts with
|
||||
Window int `json:"window"`
|
||||
Cache float64 `json:"cache"` // of its prompts, read from the cache
|
||||
Growth int `json:"growth"` // a request's prompt more than the one before it, the median
|
||||
PeakFill float64 `json:"peakFill"`
|
||||
Compacts int `json:"compacts"`
|
||||
Shares map[string]float64 `json:"shares"` // each part's share of its prompts
|
||||
MCP int `json:"mcp"` // MCP servers' tool definitions, the median
|
||||
Score int `json:"score"`
|
||||
Scores []ctxScore `json:"scores"`
|
||||
Tags []ctxTag `json:"tags"`
|
||||
Models []string `json:"models"`
|
||||
LatestID int64 `json:"latestId"`
|
||||
LatestTime time.Time `json:"latestTime"`
|
||||
}
|
||||
|
||||
type ctxJSON struct {
|
||||
Days int `json:"days"`
|
||||
Agents []ctxAgent `json:"agents"`
|
||||
Sessions []ctxSession `json:"sessions"`
|
||||
}
|
||||
|
||||
// ctxSessions is how many sessions are listed, the latest; ctxPoints how
|
||||
// many requests a session's line keeps, the latest.
|
||||
const (
|
||||
ctxSessions = 80
|
||||
ctxPoints = 160
|
||||
)
|
||||
|
||||
func median(xs []int) int {
|
||||
if len(xs) == 0 {
|
||||
return 0
|
||||
}
|
||||
s := slices.Clone(xs)
|
||||
slices.Sort(s)
|
||||
return s[len(s)/2]
|
||||
}
|
||||
|
||||
func quantile(xs []int, q float64) int {
|
||||
if len(xs) == 0 {
|
||||
return 0
|
||||
}
|
||||
s := slices.Clone(xs)
|
||||
slices.Sort(s)
|
||||
return s[min(len(s)-1, int(float64(len(s))*q))]
|
||||
}
|
||||
|
||||
func clamp01(x float64) float64 { return math.Max(0, math.Min(1, x)) }
|
||||
|
||||
func partTokens(p *gateway.Prompt, kinds ...string) int {
|
||||
n := 0
|
||||
for _, x := range p.Parts {
|
||||
if slices.Contains(kinds, x.Kind) {
|
||||
n += x.Tokens
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func mcpTokens(p *gateway.Prompt) int {
|
||||
n := 0
|
||||
for _, x := range p.Parts {
|
||||
if x.Kind != gateway.PartTools {
|
||||
continue
|
||||
}
|
||||
for _, it := range x.Items {
|
||||
if it.Tag == "mcp" {
|
||||
n += it.Tokens
|
||||
}
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func cacheOf(r gateway.Route) int {
|
||||
if len(r.Usage) == 0 {
|
||||
return 0
|
||||
}
|
||||
return r.Usage[len(r.Usage)-1].CacheRead
|
||||
}
|
||||
|
||||
// sessionOfRoute is the conversation a route is part of.
|
||||
func sessionOfRoute(r gateway.Route) string {
|
||||
switch {
|
||||
case r.ParentSession != "":
|
||||
return r.ParentSession
|
||||
case r.Session != "":
|
||||
return r.Session
|
||||
}
|
||||
return r.Conv
|
||||
}
|
||||
|
||||
// compacted says a prompt is a compaction of the one before it: under 60%
|
||||
// of a prompt of some size.
|
||||
func compacted(prev, next int) bool { return prev >= 20000 && next < prev*6/10 }
|
||||
|
||||
// contextOf works the agents and their sessions out of their routes.
|
||||
func contextOf(routes []gateway.Route, days int) ctxJSON {
|
||||
sort.SliceStable(routes, func(i, j int) bool { return routes[i].Time.Before(routes[j].Time) })
|
||||
out := ctxJSON{Days: days, Agents: []ctxAgent{}, Sessions: []ctxSession{}}
|
||||
type agentAcc struct {
|
||||
a ctxAgent
|
||||
prompts, bases, growths []int
|
||||
mcps []int
|
||||
cached, counted int
|
||||
parts map[string]int
|
||||
models map[string]int
|
||||
sessions map[string]bool
|
||||
fills []float64
|
||||
}
|
||||
agents := map[string]*agentAcc{}
|
||||
sess := map[string]*ctxSession{}
|
||||
var order []string
|
||||
for _, r := range routes {
|
||||
if !r.Done || r.Agent == "" {
|
||||
continue
|
||||
}
|
||||
acc := agents[r.Agent]
|
||||
if acc == nil {
|
||||
acc = &agentAcc{a: ctxAgent{Agent: r.Agent}, parts: map[string]int{}, models: map[string]int{}, sessions: map[string]bool{}}
|
||||
agents[r.Agent] = acc
|
||||
}
|
||||
acc.a.Calls++
|
||||
if r.Status >= 400 || r.Error != "" {
|
||||
acc.a.Errors++
|
||||
continue
|
||||
}
|
||||
p := r.Prompt
|
||||
if p == nil || p.Tokens <= 0 || r.Kind != "" {
|
||||
// a title or another side call is neither the conversation's
|
||||
// nor what the agent's prompts are like
|
||||
continue
|
||||
}
|
||||
window := p.Window
|
||||
if window <= 0 {
|
||||
window = defaultWindow
|
||||
}
|
||||
acc.a.Requests++
|
||||
acc.prompts = append(acc.prompts, p.Tokens)
|
||||
acc.bases = append(acc.bases, partTokens(p, gateway.PartSystem, gateway.PartTools, gateway.PartMemory))
|
||||
acc.mcps = append(acc.mcps, mcpTokens(p))
|
||||
for _, x := range p.Parts {
|
||||
acc.parts[x.Kind] += x.Tokens
|
||||
}
|
||||
if p.Counted {
|
||||
acc.counted += p.Tokens
|
||||
acc.cached += min(cacheOf(r), p.Tokens)
|
||||
}
|
||||
acc.models[r.Model]++
|
||||
if r.Time.After(acc.a.LatestTime) {
|
||||
acc.a.LatestTime, acc.a.LatestID = r.Time, r.ID
|
||||
}
|
||||
acc.a.Window = max(acc.a.Window, window)
|
||||
key := sessionOfRoute(r)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
acc.sessions[key] = true
|
||||
sk := r.Agent + "\x00" + key
|
||||
s := sess[sk]
|
||||
if s == nil {
|
||||
s = &ctxSession{Agent: r.Agent, Key: key, First: r.Time, Points: []ctxPoint{}}
|
||||
sess[sk] = s
|
||||
order = append(order, sk)
|
||||
}
|
||||
if n := len(s.Points); n > 0 {
|
||||
prev := s.Points[n-1].Tokens
|
||||
if compacted(prev, p.Tokens) {
|
||||
s.Compacts++
|
||||
acc.a.Compacts++
|
||||
} else if d := p.Tokens - prev; d > 0 {
|
||||
acc.growths = append(acc.growths, d)
|
||||
}
|
||||
}
|
||||
s.Points = append(s.Points, ctxPoint{ID: r.ID, Time: r.Time, Tokens: p.Tokens, Window: p.Window, Cache: cacheOf(r)})
|
||||
s.Requests++
|
||||
s.Last, s.Latest, s.LatestID, s.Model = r.Time, p, r.ID, r.Model
|
||||
s.Peak = max(s.Peak, p.Tokens)
|
||||
s.Window = max(s.Window, window)
|
||||
}
|
||||
// the sessions, the latest first
|
||||
slices.SortFunc(order, func(a, b string) int { return sess[b].Last.Compare(sess[a].Last) })
|
||||
var codexIDs []string
|
||||
for _, sk := range order {
|
||||
s := sess[sk]
|
||||
if acc := agents[s.Agent]; acc != nil && s.Requests > 1 {
|
||||
acc.fills = append(acc.fills, float64(s.Peak)/float64(s.Window))
|
||||
}
|
||||
if len(out.Sessions) < ctxSessions {
|
||||
if len(s.Points) > ctxPoints {
|
||||
s.Points = s.Points[len(s.Points)-ctxPoints:]
|
||||
}
|
||||
out.Sessions = append(out.Sessions, *s)
|
||||
if s.Agent == "codex" {
|
||||
codexIDs = append(codexIDs, s.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(codexIDs) > 0 {
|
||||
titles := sessions.CodexTitles(codexIDs)
|
||||
for i := range out.Sessions {
|
||||
if out.Sessions[i].Agent == "codex" {
|
||||
out.Sessions[i].Title = titles[out.Sessions[i].Key]
|
||||
}
|
||||
}
|
||||
}
|
||||
// every other agent's sessions go by the name the Sessions page shows
|
||||
untitled := map[string]int{}
|
||||
for i, s := range out.Sessions {
|
||||
if s.Title == "" {
|
||||
untitled[s.Agent+"\x00"+s.Key] = i
|
||||
}
|
||||
}
|
||||
if len(untitled) > 0 {
|
||||
for _, s := range sessions.List(0) {
|
||||
if i, ok := untitled[s.Agent+"\x00"+s.ID]; ok && s.Title != "" {
|
||||
out.Sessions[i].Title = s.Title
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, acc := range agents {
|
||||
a := &acc.a
|
||||
if a.Requests == 0 {
|
||||
continue
|
||||
}
|
||||
a.Sessions = len(acc.sessions)
|
||||
a.Median, a.P90 = median(acc.prompts), quantile(acc.prompts, 0.9)
|
||||
a.Baseline, a.Growth, a.MCP = median(acc.bases), median(acc.growths), median(acc.mcps)
|
||||
if acc.counted > 0 {
|
||||
a.Cache = float64(acc.cached) / float64(acc.counted)
|
||||
}
|
||||
for _, f := range acc.fills {
|
||||
a.PeakFill = math.Max(a.PeakFill, f)
|
||||
}
|
||||
total := 0
|
||||
for _, n := range acc.parts {
|
||||
total += n
|
||||
}
|
||||
a.Shares = map[string]float64{}
|
||||
for k, n := range acc.parts {
|
||||
a.Shares[k] = float64(n) / float64(max(1, total))
|
||||
}
|
||||
for m := range acc.models {
|
||||
a.Models = append(a.Models, m)
|
||||
}
|
||||
slices.SortFunc(a.Models, func(x, y string) int { return acc.models[y] - acc.models[x] })
|
||||
if len(a.Models) > 3 {
|
||||
a.Models = a.Models[:3]
|
||||
}
|
||||
scoreAgent(a, acc.counted > 0)
|
||||
out.Agents = append(out.Agents, *a)
|
||||
}
|
||||
slices.SortFunc(out.Agents, func(x, y ctxAgent) int {
|
||||
if x.Requests != y.Requests {
|
||||
return y.Requests - x.Requests
|
||||
}
|
||||
return y.LatestTime.Compare(x.LatestTime)
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// scoreAgent scores an agent's use of its context out of 100, and tags it:
|
||||
// - cache, 35: the share of its prompts its vendor read from the cache,
|
||||
// full at 90%
|
||||
// - lean, 25: what every request starts with (system prompt, tools,
|
||||
// memory) against the window, full at 5% or less, none at 30%
|
||||
// - pace, 20: how much a request adds to the one before it, full at 1%
|
||||
// of the window or less, none at 6%
|
||||
// - reliable, 20: its requests that didn't fail, none at 20% failing
|
||||
func scoreAgent(a *ctxAgent, counted bool) {
|
||||
w := float64(max(a.Window, 1))
|
||||
cache := 0.5 // not known: neither good nor bad
|
||||
if counted {
|
||||
cache = clamp01(a.Cache / 0.9)
|
||||
}
|
||||
lean := clamp01(1 - (float64(a.Baseline)/w-0.05)/0.25)
|
||||
pace := clamp01(1 - (float64(a.Growth)/w-0.01)/0.05)
|
||||
errRate := float64(a.Errors) / float64(max(1, a.Calls))
|
||||
reliable := clamp01(1 - errRate/0.2)
|
||||
a.Scores = []ctxScore{
|
||||
{Key: "cache", Points: math.Round(cache * 35), Most: 35},
|
||||
{Key: "lean", Points: math.Round(lean * 25), Most: 25},
|
||||
{Key: "pace", Points: math.Round(pace * 20), Most: 20},
|
||||
{Key: "reliable", Points: math.Round(reliable * 20), Most: 20},
|
||||
}
|
||||
score := 0.0
|
||||
for _, s := range a.Scores {
|
||||
score += s.Points
|
||||
}
|
||||
a.Score = int(score)
|
||||
|
||||
tag := func(key, tone string, v float64) { a.Tags = append(a.Tags, ctxTag{Key: key, Tone: tone, Value: v}) }
|
||||
a.Tags = []ctxTag{}
|
||||
switch {
|
||||
case counted && a.Cache >= 0.8:
|
||||
tag("cache-friendly", "good", a.Cache)
|
||||
case counted && a.Cache < 0.4 && a.Requests >= 5:
|
||||
tag("cache-misses", "warn", a.Cache)
|
||||
}
|
||||
switch base := float64(a.Baseline) / w; {
|
||||
case base <= 0.06:
|
||||
tag("lean", "good", float64(a.Baseline))
|
||||
case base >= 0.2:
|
||||
tag("heavy-start", "warn", float64(a.Baseline))
|
||||
}
|
||||
if a.MCP >= 5000 && float64(a.MCP) >= 0.3*float64(a.Baseline) {
|
||||
tag("mcp-heavy", "info", float64(a.MCP))
|
||||
}
|
||||
if s := a.Shares[gateway.PartResults]; s >= 0.3 {
|
||||
tag("tool-heavy", "info", s)
|
||||
}
|
||||
if s := a.Shares[gateway.PartFiles]; s >= 0.25 {
|
||||
tag("file-reader", "info", s)
|
||||
}
|
||||
if s := a.Shares[gateway.PartMemory]; s >= 0.1 {
|
||||
tag("memory-heavy", "info", s)
|
||||
}
|
||||
if g := float64(a.Growth) / w; g >= 0.03 {
|
||||
tag("fills-fast", "warn", float64(a.Growth))
|
||||
}
|
||||
if a.PeakFill >= 0.85 {
|
||||
tag("near-full", "warn", a.PeakFill)
|
||||
}
|
||||
if a.Compacts > 0 {
|
||||
tag("compacts", "info", float64(a.Compacts))
|
||||
}
|
||||
errRate = float64(a.Errors) / float64(max(1, a.Calls))
|
||||
switch {
|
||||
case a.Calls >= 20 && errRate < 0.01:
|
||||
tag("steady", "good", errRate)
|
||||
case a.Calls >= 5 && errRate >= 0.1:
|
||||
tag("flaky", "warn", errRate)
|
||||
}
|
||||
}
|
||||
|
||||
// contextRoutes reads the routes of the days asked for: the history kept,
|
||||
// and the live trace's not yet in it.
|
||||
func contextRoutes(r *http.Request, days int) []gateway.Route {
|
||||
seen := map[int64]bool{}
|
||||
var out []gateway.Route
|
||||
now := time.Now()
|
||||
for i := days - 1; i >= 0; i-- {
|
||||
day := now.AddDate(0, 0, -i).Format("2006-01-02")
|
||||
_, rs, _ := gateway.History(day)
|
||||
for _, x := range rs {
|
||||
if !seen[x.ID] {
|
||||
seen[x.ID] = true
|
||||
out = append(out, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
if gw := served.Load(); gw != nil {
|
||||
for _, x := range gw.Trace(r.Context(), 0, 0).Routes {
|
||||
if x.Done && !seen[x.ID] {
|
||||
seen[x.ID] = true
|
||||
out = append(out, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func contextRoutesAPI(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/context", func(rw http.ResponseWriter, r *http.Request) {
|
||||
days, _ := strconv.Atoi(r.URL.Query().Get("days"))
|
||||
if days <= 0 || days > 30 {
|
||||
days = 7
|
||||
}
|
||||
writeJSON(rw, contextOf(contextRoutes(r, days), days))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/gateway"
|
||||
"github.com/yetone/magpie/internal/sessions"
|
||||
)
|
||||
|
||||
// ctxRoute is a done request of agent in session whose prompt holds base
|
||||
// (system prompt, tools, memory) and chat, the cache read of it counted.
|
||||
func ctxRoute(id int64, agent, session string, base, chat, window, cached int) gateway.Route {
|
||||
return gateway.Route{
|
||||
ID: id, Time: time.Unix(1_790_000_000+id*60, 0), Agent: agent, Session: session, Model: agent + "-model", Done: true, Status: 200,
|
||||
Usage: []gateway.RouteUsage{{CacheRead: cached}},
|
||||
Prompt: &gateway.Prompt{Window: window, Tokens: base + chat, Counted: true, Parts: []gateway.PromptPart{
|
||||
{Kind: gateway.PartSystem, Tokens: base / 2},
|
||||
{Kind: gateway.PartTools, Tokens: base - base/2, Items: []gateway.PromptItem{{Name: "linear", Tag: "mcp", Tokens: base * 2 / 5}}},
|
||||
{Kind: gateway.PartChat, Tokens: chat},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func tagKeys(a ctxAgent) []string {
|
||||
var keys []string
|
||||
for _, tg := range a.Tags {
|
||||
keys = append(keys, tg.Key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// An agent that starts small, grows slowly and is read from the cache
|
||||
// scores high and is told lean and cache-friendly; its session's line is
|
||||
// its requests', oldest first.
|
||||
func TestContextOfALeanAgent(t *testing.T) {
|
||||
var routes []gateway.Route
|
||||
for i, chat := range []int{1000, 3000, 6000, 8000} {
|
||||
total := 12000 + chat
|
||||
routes = append(routes, ctxRoute(int64(i+1), "codex", "s1", 12000, chat, 272000, total*9/10))
|
||||
}
|
||||
// a title call is no part of what its prompts are like
|
||||
title := ctxRoute(9, "codex", "s1", 500, 0, 272000, 0)
|
||||
title.Kind = "title"
|
||||
routes = append(routes, title)
|
||||
|
||||
out := contextOf(routes, 7)
|
||||
if len(out.Agents) != 1 || len(out.Sessions) != 1 {
|
||||
t.Fatalf("agents %d sessions %d", len(out.Agents), len(out.Sessions))
|
||||
}
|
||||
a := out.Agents[0]
|
||||
if a.Requests != 4 || a.Baseline != 12000 || a.Window != 272000 {
|
||||
t.Fatalf("requests %d baseline %d window %d", a.Requests, a.Baseline, a.Window)
|
||||
}
|
||||
if a.Score < 90 {
|
||||
t.Fatalf("score %d %+v", a.Score, a.Scores)
|
||||
}
|
||||
for _, want := range []string{"lean", "cache-friendly"} {
|
||||
if !slices.Contains(tagKeys(a), want) {
|
||||
t.Fatalf("tags %v, want %s", tagKeys(a), want)
|
||||
}
|
||||
}
|
||||
s := out.Sessions[0]
|
||||
if s.Requests != 4 || s.Peak != 20000 || s.LatestID != 4 || len(s.Points) != 4 || s.Points[0].Tokens != 13000 {
|
||||
t.Fatalf("session %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
// One that starts with most of a window of MCP tools, misses the cache,
|
||||
// compacts and fails often scores low and is told so.
|
||||
func TestContextOfAHeavyAgent(t *testing.T) {
|
||||
var routes []gateway.Route
|
||||
chats := []int{10000, 40000, 100000, 5000, 20000, 30000}
|
||||
for i, chat := range chats {
|
||||
routes = append(routes, ctxRoute(int64(i+1), "claude", "s1", 70000, chat, 200000, 1000))
|
||||
}
|
||||
for i := range 3 {
|
||||
routes = append(routes, gateway.Route{ID: int64(20 + i), Time: time.Unix(1_790_000_000, 0), Agent: "claude", Done: true, Status: 529})
|
||||
}
|
||||
a := contextOf(routes, 7).Agents[0]
|
||||
if a.Compacts != 1 || a.Errors != 3 || a.Calls != 9 {
|
||||
t.Fatalf("compacts %d errors %d calls %d", a.Compacts, a.Errors, a.Calls)
|
||||
}
|
||||
if a.Score >= 50 {
|
||||
t.Fatalf("score %d %+v", a.Score, a.Scores)
|
||||
}
|
||||
for _, want := range []string{"cache-misses", "heavy-start", "mcp-heavy", "compacts", "flaky", "near-full"} {
|
||||
if !slices.Contains(tagKeys(a), want) {
|
||||
t.Fatalf("tags %v, want %s", tagKeys(a), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An agent whose vendor doesn't count the cache is scored neither up nor
|
||||
// down for it, and not told cache-friendly or cache-misses.
|
||||
func TestContextCacheNotKnown(t *testing.T) {
|
||||
var routes []gateway.Route
|
||||
for i := range 6 {
|
||||
r := ctxRoute(int64(i+1), "gemini", "s1", 8000, 1000*i, 1000000, 0)
|
||||
r.Prompt.Counted, r.Usage = false, nil
|
||||
routes = append(routes, r)
|
||||
}
|
||||
a := contextOf(routes, 1).Agents[0]
|
||||
if a.Scores[0].Key != "cache" || a.Scores[0].Points != 18 {
|
||||
t.Fatalf("cache score %+v", a.Scores[0])
|
||||
}
|
||||
for _, k := range tagKeys(a) {
|
||||
if k == "cache-friendly" || k == "cache-misses" {
|
||||
t.Fatalf("tags %v", tagKeys(a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A session goes by the name the Sessions page shows for it: here the one
|
||||
// a Claude Code session was renamed to.
|
||||
func TestContextSessionTitles(t *testing.T) {
|
||||
h := sandboxHome(t)
|
||||
claude := filepath.Join(h, ".claude")
|
||||
proj := filepath.Join(claude, "projects", "-work")
|
||||
if err := os.MkdirAll(proj, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const id = "11111111-2222-3333-4444-555555555555"
|
||||
body := `{"parentUuid":null,"isSidechain":false,"type":"user","message":{"role":"user","content":"hi"},"uuid":"u","timestamp":"2026-09-20T10:00:00.000Z","cwd":"/work","sessionId":"` + id + `"}` + "\n" +
|
||||
`{"parentUuid":"u","isSidechain":false,"message":{"model":"claude-sonnet-5","id":"msg_1","type":"message","role":"assistant","content":[{"type":"text","text":"done"}],"usage":{"input_tokens":10,"output_tokens":2}},"requestId":"req_1","type":"assistant","uuid":"a","timestamp":"2026-09-20T10:00:01.000Z","cwd":"/work","sessionId":"` + id + `"}` + "\n" +
|
||||
`{"type":"custom-title","customTitle":"Port the parser","sessionId":"` + id + `"}` + "\n"
|
||||
if err := os.WriteFile(filepath.Join(proj, id+".jsonl"), []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("CLAUDE_CONFIG_DIR", claude)
|
||||
sessions.Reset()
|
||||
t.Cleanup(sessions.Reset)
|
||||
|
||||
out := contextOf([]gateway.Route{
|
||||
ctxRoute(1, "claude", id, 20000, 1000, 200000, 0),
|
||||
ctxRoute(2, "claude", "not-a-session", 20000, 1000, 200000, 0),
|
||||
}, 7)
|
||||
titles := map[string]string{}
|
||||
for _, s := range out.Sessions {
|
||||
titles[s.Key] = s.Title
|
||||
}
|
||||
if titles[id] != "Port the parser" || titles["not-a-session"] != "" {
|
||||
t.Fatalf("titles %v", titles)
|
||||
}
|
||||
}
|
||||
@@ -706,6 +706,7 @@ func providerRoutes(mux *http.ServeMux, w Windows) {
|
||||
importAppsRoutes(mux)
|
||||
pluginRoutes(mux, w)
|
||||
traceRoutes(mux)
|
||||
contextRoutesAPI(mux)
|
||||
groupRoutes(mux)
|
||||
// how each key's or account's requests stand under its limit on
|
||||
// requests at once (#892), read every two seconds while a provider's
|
||||
|
||||
@@ -595,6 +595,15 @@ cny (#212): the Usage page's total converts, the row's tooltip carries the
|
||||
rate, picking it with the settings list scrolled well down moves nothing,
|
||||
and the choice survives a reload — in English and Chinese.
|
||||
|
||||
`context-window.test.cjs` opens Usage's Context tab: each agent's score, its
|
||||
grade and tags, its sessions counted one or many, the score bars toned by how
|
||||
much of them there is; a refresh with the same answer keeps the pane; a
|
||||
session opened draws its latest request's window in 400 cells without moving
|
||||
the page, a cell hovered names its part and what is largest in it, and the
|
||||
Tools tab of the contents lists only tools; nothing scrolls sideways — in
|
||||
Chromium and WebKit, in English, Chinese, Japanese and German, at 1100px and
|
||||
420px.
|
||||
|
||||
`tray-usages.test.cjs` picks several of the Usage page's cards for the menu
|
||||
bar in Settings: the menu keeps open as Codex and Claude Code are ticked
|
||||
beside Copilot, posts nothing till it closes and then the three once, in the
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
// Run with Node's test runner and Playwright on the module path; see README.md.
|
||||
// Usage's Context tab: each agent's prompts as the gateway read them, a
|
||||
// score out of 100 with its grade, tags with what they are told by, and
|
||||
// its sessions' fill; a session opened draws its latest request's context
|
||||
// window — 400 cells by what the prompt holds, a cell hovered telling its
|
||||
// part and the largest things in it, the contents by part. The refresh
|
||||
// that brings the same answer keeps the pane as it was. In every language,
|
||||
// at a desk and a phone's width.
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const { chromium, webkit } = require("playwright");
|
||||
|
||||
const assets = path.resolve(__dirname, "../assets");
|
||||
const now = Date.now();
|
||||
const ago = (m) => new Date(now - m * 60000).toISOString();
|
||||
|
||||
const latest = {
|
||||
window: 272000, tokens: 21400, counted: true, turns: 3,
|
||||
parts: [
|
||||
{ kind: "system", tokens: 5200, items: [{ name: "prompt", tokens: 4900 }, { name: "environment_context", tokens: 300 }] },
|
||||
{ kind: "tools", tokens: 5000, items: [{ name: "multi_agent_v1", tag: "namespace", n: 5, tokens: 2900 }, { name: "exec_command", tokens: 2100 }] },
|
||||
{ kind: "memory", tokens: 3300, items: [{ name: "skills", tag: "skills", tokens: 3300 }] },
|
||||
{ kind: "files", tokens: 5900, items: [{ name: "/work/app/prompt.go", tag: "shell", tokens: 2950 }, { name: "/work/app/context.go", tag: "shell", tokens: 2950 }] },
|
||||
{ kind: "results", tokens: 1200, items: [{ name: "exec_command", tag: "result", n: 4, tokens: 1200 }] },
|
||||
{ kind: "chat", tokens: 800, items: [{ name: "turn", tag: "turn", n: 3, tokens: 800 }] },
|
||||
],
|
||||
};
|
||||
const context = (days) => ({
|
||||
days,
|
||||
agents: [
|
||||
{ agent: "codex", requests: 9, sessions: 1, errors: 0, calls: 9, median: 17700, p90: 21400, baseline: 13300, window: 272000, cache: 0.74, growth: 228, peakFill: 0.08, compacts: 0,
|
||||
shares: { system: 0.29, tools: 0.28, memory: 0.18, files: 0.18, results: 0.04, chat: 0.03 }, mcp: 0,
|
||||
score: 94, scores: [{ key: "cache", points: 29, most: 35 }, { key: "lean", points: 25, most: 25 }, { key: "pace", points: 20, most: 20 }, { key: "reliable", points: 20, most: 20 }],
|
||||
tags: [{ key: "lean", tone: "good", value: 13300 }, { key: "memory-heavy", tone: "info", value: 0.18 }],
|
||||
models: ["live/codex/gpt-5.6-luna"], latestId: 19, latestTime: ago(4) },
|
||||
{ agent: "claude", requests: 12, sessions: 2, errors: 3, calls: 15, median: 140000, p90: 180000, baseline: 70000, window: 200000, cache: 0.2, growth: 9000, peakFill: 0.9, compacts: 1,
|
||||
shares: { system: 0.1, tools: 0.6, memory: 0.05, files: 0.1, results: 0.1, chat: 0.05 }, mcp: 28000,
|
||||
score: 31, scores: [{ key: "cache", points: 8, most: 35 }, { key: "lean", points: 3, most: 25 }, { key: "pace", points: 10, most: 20 }, { key: "reliable", points: 10, most: 20 }],
|
||||
tags: [{ key: "cache-misses", tone: "warn", value: 0.2 }, { key: "heavy-start", tone: "warn", value: 70000 }, { key: "mcp-heavy", tone: "info", value: 28000 }],
|
||||
models: ["claude-sonnet-5"], latestId: 30, latestTime: ago(9) },
|
||||
],
|
||||
sessions: [
|
||||
{ agent: "codex", key: "01a117b1-aecd-76e2-9b2f-3c1d", title: "Port the parser", model: "live/codex/gpt-5.6-luna", first: ago(12), last: ago(4), requests: 3, peak: 21400, window: 272000,
|
||||
latest, latestId: 19, points: [{ id: 17, time: ago(12), tokens: 13000, cache: 0 }, { id: 18, time: ago(8), tokens: 17700, cache: 12000 }, { id: 19, time: ago(4), tokens: 21400, cache: 20900 }] },
|
||||
{ agent: "claude", key: "ce0c93f6-1111-2222-3333-444444444444", model: "claude-sonnet-5", first: ago(30), last: ago(9), requests: 1, peak: 180000, window: 200000,
|
||||
latest: { ...latest, window: 200000, tokens: 180000 }, latestId: 30, points: [{ id: 30, time: ago(9), tokens: 180000 }] },
|
||||
],
|
||||
});
|
||||
|
||||
// the reader scrolls (the wheel) until what they'll point at is in view:
|
||||
// the app puts back a scroll that isn't the reader's, a test's own included
|
||||
async function wheelTo(page, l) {
|
||||
await page.mouse.move(200, 300);
|
||||
for (let i = 0; i < 40 && await l.evaluate((b) => b.getBoundingClientRect().bottom > innerHeight - 70); i++) {
|
||||
await page.mouse.wheel(0, 120);
|
||||
await page.waitForTimeout(50);
|
||||
}
|
||||
// a wheel scroll is eased: hovered while it still runs, the cell moves out
|
||||
// from under the pointer and its tip closes again. Wait until it stops.
|
||||
const view = page.locator("#view-usage");
|
||||
for (let last = -1, i = 0; i < 40; i++) {
|
||||
await page.waitForTimeout(100);
|
||||
const now = await view.evaluate((v) => v.scrollTop);
|
||||
if (now === last) break;
|
||||
last = now;
|
||||
}
|
||||
}
|
||||
|
||||
const words = {
|
||||
en: { tab: "Context", excellent: "Excellent", care: "Needs care", lean: "Lean start", heavy: "Heavy start", one: "1 session", two: "2 sessions", card: "Context window", tools: "Tools", free: "Free space" },
|
||||
zh: { tab: "上下文", excellent: "优秀", care: "待改进", lean: "起步轻", heavy: "起步重", one: "1 个会话", two: "2 个会话", card: "上下文窗口", tools: "工具", free: "剩余空间" },
|
||||
"zh-TW": { tab: "上下文", excellent: "優秀", care: "待改進", lean: "起步輕", heavy: "起步重", one: "1 個會話", two: "2 個會話", card: "上下文視窗", tools: "工具", free: "剩餘空間" },
|
||||
ja: { tab: "コンテキスト", excellent: "優秀", care: "要改善", lean: "軽いスタート", heavy: "重いスタート", one: "1 件のセッション", two: "2 件のセッション", card: "コンテキストウィンドウ", tools: "ツール", free: "空き" },
|
||||
de: { tab: "Kontext", excellent: "Ausgezeichnet", care: "Ausbaufähig", lean: "Schlanker Start", heavy: "Schwerer Start", one: "1 Sitzung", two: "2 Sitzungen", card: "Kontextfenster", tools: "Tools", free: "Frei" },
|
||||
};
|
||||
|
||||
function serve(lang, asked) {
|
||||
const state = { agents: [{ id: "claude", name: "Claude Code", path: "/test/claude", fields: [] }, { id: "codex", name: "Codex", path: "/test/codex", fields: [] }], profiles: [], settings: { lang, theme: "light" } };
|
||||
return async (r) => {
|
||||
const url = new URL(r.request().url());
|
||||
const json = (data) => r.fulfill({ json: data });
|
||||
if (url.pathname === "/boot.js") return r.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:true};` });
|
||||
if (url.pathname === "/wails/runtime.js") return r.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
|
||||
if (url.pathname === "/api/state") return json(state);
|
||||
if (url.pathname === "/api/context") {
|
||||
asked.push(url.searchParams.get("days"));
|
||||
return json(context(+url.searchParams.get("days")));
|
||||
}
|
||||
if (url.pathname === "/api/gateway/trace") {
|
||||
if (url.searchParams.get("wait")) return new Promise(() => {});
|
||||
return json({ mine: true, now: new Date().toISOString(), seq: 0, totals: { requests: 0, rerouted: 0, errors: 0 }, routes: [] });
|
||||
}
|
||||
if (url.pathname === "/api/providers") return json({ providers: [], presets: [], excluded: [], gateway: { running: true, window: true, url: "http://127.0.0.1:3999" } });
|
||||
if (url.pathname === "/api/usage/quotas") return json([]);
|
||||
if (url.pathname === "/api/groups") return json({ groups: [], models: [] });
|
||||
if (url.pathname === "/api/plugins") return json({ plugins: [] });
|
||||
if (url.pathname.startsWith("/api/")) return json({});
|
||||
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
|
||||
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
|
||||
await r.fulfill({ body: await fs.readFile(file).catch(() => ""), contentType, status: 200 });
|
||||
};
|
||||
}
|
||||
|
||||
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
||||
for (const lang of Object.keys(words)) {
|
||||
for (const width of [1100, 420]) {
|
||||
const w = words[lang];
|
||||
test(`${engine} ${lang} ${width}px: the Context tab`, async (t) => {
|
||||
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
|
||||
t.after(() => browser.close());
|
||||
const page = await (await browser.newContext({ viewport: { width, height: 1400 } })).newPage();
|
||||
page.setDefaultTimeout(5000);
|
||||
const errors = [], asked = [];
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
await page.route("**/*", serve(lang, asked));
|
||||
await page.addInitScript(() => { try { if (!sessionStorage.getItem("set")) { localStorage.clear(); localStorage.setItem("magpie.usageTab", "context"); sessionStorage.setItem("set", "1"); } } catch {} });
|
||||
await page.goto("http://magpie.test/?view=usage");
|
||||
assert.equal(await page.locator("#usageTab button.on").innerText(), w.tab);
|
||||
|
||||
// each agent: its score, grade and tags; one session or more
|
||||
const codex = page.locator(".ctx-agent", { hasText: "Codex" });
|
||||
const claude = page.locator(".ctx-agent", { hasText: "Claude Code" });
|
||||
await codex.waitFor();
|
||||
assert.equal(asked[0], "7", "a week at first");
|
||||
assert.match(await codex.locator(".ctx-ring").innerText(), /94/);
|
||||
assert.equal((await codex.locator(".ctx-grade-word").innerText()).trim(), w.excellent);
|
||||
assert.equal((await claude.locator(".ctx-grade-word").innerText()).trim(), w.care);
|
||||
assert.ok(await codex.locator(".ctx-chip", { hasText: w.lean }).isVisible());
|
||||
assert.ok(await claude.locator(".ctx-chip", { hasText: w.heavy }).isVisible());
|
||||
assert.match(await codex.locator(".ctx-who-sub").innerText(), new RegExp(w.one));
|
||||
assert.match(await claude.locator(".ctx-who-sub").innerText(), new RegExp(w.two));
|
||||
// a score bar is toned by how much of it there is
|
||||
assert.equal(await codex.locator(".ctx-score.ok").count(), 4);
|
||||
assert.equal(await claude.locator(".ctx-score.bad").count(), 2);
|
||||
assert.equal(await claude.locator(".ctx-score.warn").count(), 2);
|
||||
|
||||
// the refresh that brings the same answer keeps the pane
|
||||
const before = await page.locator(".ctx-agent").first().elementHandle();
|
||||
await page.evaluate(() => window.loadContext());
|
||||
assert.ok(await before.evaluate((e) => e.isConnected), "the same answer redrew the pane");
|
||||
|
||||
// a session opened draws its latest request's window
|
||||
const row = page.locator(".ctx-sess-row", { hasText: "Port the parser" });
|
||||
await wheelTo(page, row);
|
||||
const y = await page.evaluate(() => document.querySelector("#view-usage").scrollTop);
|
||||
await row.click();
|
||||
const card = page.locator(".ctx-sess.open .ctx-card");
|
||||
await card.waitFor();
|
||||
assert.equal(await page.evaluate(() => document.querySelector("#view-usage").scrollTop), y, "a click doesn't move the page");
|
||||
assert.equal(await row.getAttribute("aria-expanded"), "true");
|
||||
assert.match(await card.locator(".ctx-head").innerText(), new RegExp(w.card));
|
||||
assert.equal(await card.locator(".ctx-waffle > i").count(), 400);
|
||||
assert.match(await card.locator(".ctx-legend").innerText(), new RegExp(w.free));
|
||||
|
||||
// a cell hovered tells its part and what is largest in it, and
|
||||
// dims the others
|
||||
const cell = card.locator(".ctx-waffle > i.k-tools").first();
|
||||
await wheelTo(page, cell);
|
||||
await cell.hover();
|
||||
const tip = card.locator(".ctx-tip");
|
||||
await tip.waitFor({ state: "visible" });
|
||||
assert.match(await tip.innerText(), new RegExp(w.tools));
|
||||
assert.match(await tip.innerText(), /multi_agent_v1/);
|
||||
assert.equal(await card.locator(".ctx-waffle").getAttribute("data-f"), "tools");
|
||||
|
||||
// the contents by part: a file by its name, its folder beside it
|
||||
const contents = card.locator(".ctx-contents");
|
||||
const seg = contents.locator(".segs button", { hasText: w.tools }).first();
|
||||
await wheelTo(page, seg);
|
||||
await seg.click();
|
||||
const names = await contents.locator(".ctx-row .ctx-name").allInnerTexts();
|
||||
assert.ok(names.some((n) => n.includes("multi_agent_v1")) && !names.some((n) => n.includes("prompt.go")), names.join(" | "));
|
||||
|
||||
// nothing wider than the window
|
||||
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), "the page scrolls sideways");
|
||||
assert.deepEqual(errors, []);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,6 +227,7 @@ magpie claude group/opus-anywhere <span class="c"># これを使う</span></p
|
||||
<img class="for-dark" src="/img/usage-ja-dark.png" width="2080" height="1004" loading="lazy" alt="使用量タブ:残高、トークンとコストの合計、30 日間のグラフ">
|
||||
<figcaption>ゲートウェイを通ったすべての呼び出しの残高、トークン、キャッシュヒット、コスト。</figcaption>
|
||||
</figure>
|
||||
<p><b>コンテキスト。</b> Usage の <span class="ui">Context</span> タブでは、ゲートウェイを通るときに読み取った各エージェントのプロンプトの中身を表示します。システムプロンプト、ツール定義(MCP サーバーのツールはまとめて 1 つ)、指示ファイルと skills、ツールが読み込んだファイル、ツールのその他の出力、会話が、モデルのコンテキストウィンドウのどれだけを占めるかがわかります。各エージェントには 100 点満点のスコア(プロンプトのうちキャッシュから読まれた割合、スタートの軽さ、埋まる速さ、失敗の頻度)と、スコアの根拠を示す「軽いスタート」「MCP が重い」などのタグが付きます。セッションを開くと最新リクエストのウィンドウをマスごとに見られ、<span class="ui">Routing</span> ページでも選んだリクエストについて同じものが表示されます。保存するのは名前だけで、プロンプトの本文は保存しません。</p>
|
||||
<p><b>プロファイル。</b> <span class="ui">Agents</span> タブの下部にある <span class="ui">+ Save current</span> で、全エージェントの設定を名前を付けて保存できます。チップをクリックすれば、すべてを一度に元に戻せます。</p>
|
||||
|
||||
<h2 id="cli"><a class="anchor" href="#cli">ターミナルから</a></h2>
|
||||
|
||||
@@ -227,6 +227,7 @@ magpie claude group/opus-anywhere <span class="c"># use it</span></pre>
|
||||
<img class="for-dark" src="/img/usage-dark.png" width="2080" height="842" loading="lazy" alt="The Usage tab: remaining balances, token and cost totals, and a 30-day chart">
|
||||
<figcaption>Balances, tokens, cache hits and cost for every call through the gateway.</figcaption>
|
||||
</figure>
|
||||
<p><b>Context.</b> Usage’s <span class="ui">Context</span> tab shows what each agent’s prompts hold, read as they pass through the gateway: its system prompt, tool definitions (an MCP server’s as one), instruction files and skills, the files its tools read, their other output and the conversation, as parts of the model’s context window. Each agent gets a score out of 100 — how much of its prompt the cache reads, how lean it starts, how fast it fills and how often it fails — and tags such as <i>Lean start</i> or <i>MCP-heavy</i> that say what the score was told by. Open a session to see its latest request’s window, cell by cell; the <span class="ui">Routing</span> page shows the same for any request you pick. Only names are kept, never your prompt’s text.</p>
|
||||
<p><b>Profiles.</b> At the bottom of the <span class="ui">Agents</span> tab, <span class="ui">+ Save current</span> snapshots every agent’s settings under a name; click the chip to switch everything back in one move.</p>
|
||||
|
||||
<h2 id="cli"><a class="anchor" href="#cli">From the terminal</a></h2>
|
||||
|
||||
@@ -227,6 +227,7 @@ magpie claude group/opus-anywhere <span class="c"># 使用它</span></pre>
|
||||
<img class="for-dark" src="/img/usage-zh-dark.png" width="2080" height="842" loading="lazy" alt="用量页:剩余余额、token 与费用汇总,以及 30 天趋势图">
|
||||
<figcaption>经过网关的每次调用的余额、token、缓存命中和费用。</figcaption>
|
||||
</figure>
|
||||
<p><b>上下文。</b><span class="ui">用量</span> 里的 <span class="ui">上下文</span> 标签页显示每个 Agent 的提示词里装了什么,在请求经过网关时读出:系统提示词、工具定义(一个 MCP 服务器的工具算作一项)、指令文件与 skills、工具读入的文件、工具的其他输出和对话,各占模型上下文窗口的多少。每个 Agent 有一个百分制得分——缓存读到了多少提示词、起步多轻、上下文涨得多快、失败多不多——以及「起步轻」「MCP 偏重」之类的标签,说明得分从何而来。打开一个会话,可以一格一格地看它最近一次请求的窗口;在 <span class="ui">路由</span> 页点选任意请求也能看到。magpie 只记名称,不保存提示词的文字。</p>
|
||||
<p><b>方案。</b>在 <span class="ui">Agent</span> 标签页底部点 <span class="ui">+ 保存当前</span>,把所有 Agent 的设置存成一个方案;之后点这个方案就能一键全部切回。</p>
|
||||
|
||||
<h2 id="cli"><a class="anchor" href="#cli">命令行</a></h2>
|
||||
|
||||
Reference in new issue
Block a user