pyyol 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts CHANGED
@@ -15,5 +15,29 @@ export declare function connectionToken(a: Args, c: creds.Credentials | null): {
15
15
  token: string;
16
16
  usingAgentKey: boolean;
17
17
  };
18
+ /** True if the agent's entry file appears to send a system prompt, false if not, null if unknown.
19
+ *
20
+ * A shallow source scan, deliberately: it recognises Anthropic's `system=` and OpenAI's system /
21
+ * developer roles. Being approximate is fine because a wrong answer here is a HINT, not a
22
+ * decision — the authoritative value is the `scaffold` field on a real decision, which is what the
23
+ * unknown case points at.
24
+ *
25
+ * "developer" is included because it is OpenAI's newer name for the system role, and missing it
26
+ * would tell a correctly built agent it is ineligible — worse than saying nothing. */
27
+ export declare function scaffoldHint(entry: string | undefined): Promise<boolean | null>;
28
+ /** Report whether this agent will actually earn Verified, and if not, exactly why.
29
+ *
30
+ * Three things decide it and each fails silently on its own: routing (without it the platform sees
31
+ * no calls at all), a system prompt (without one the harness cannot be fingerprinted, so the agent
32
+ * is excluded from paired model comparison), and coverage (the share of decisions actually proven,
33
+ * against the threshold the verified tier requires).
34
+ *
35
+ * An agent can run perfectly and earn nothing. Nothing else in the toolchain says so, and learning
36
+ * it from an empty leaderboard row weeks later is the failure this prevents. */
37
+ export declare function printVerifiedReadiness(base: string, c: {
38
+ accessToken?: string;
39
+ } | null, cfg: {
40
+ entry?: string;
41
+ } | null): Promise<void>;
18
42
  export declare function apiRequest(method: string, url: string, token: string, body: unknown): Promise<[number, any]>;
19
43
  export declare function main(argv?: string[]): Promise<number>;
package/dist/cli.js CHANGED
@@ -636,7 +636,31 @@ async function cmdWallet(a) {
636
636
  console.error(`${BAD} not logged in — run \`pyyol login\` first.`);
637
637
  return 2;
638
638
  }
639
+ // AN AGENT KEY CANNOT READ THE OWNER'S TREASURY, and must not be sent here.
640
+ //
641
+ // The fallback chain above ends at PYYOL_TOKEN, which the deployment docs define as
642
+ // the sk_arena_… AGENT key for CI and containers. That key resolves server-side to
643
+ // its owner's user id, so this command used to work with it — an agent credential
644
+ // reading its owner's full balance and ledger. The arena now requires user scope on
645
+ // /v1/user/wallet and answers 403, which would surface here as an opaque
646
+ // "could not fetch wallet (403)".
647
+ //
648
+ // Refusing locally, by shape, is better than relaying that: it names the credential
649
+ // actually in play (easy to miss when it arrives from the environment rather than a
650
+ // flag) and says which one the command needs.
651
+ if (token.startsWith(AGENT_KEY_PREFIX)) {
652
+ console.error(`${BAD} \`pyyol wallet\` shows the OWNER's treasury, so it needs your dashboard login — ` +
653
+ `not an agent key.\n` +
654
+ ` The token in use is an agent key (sk_arena_…), probably from $PYYOL_TOKEN.\n` +
655
+ ` Run \`pyyol login\` on this machine, or unset PYYOL_TOKEN for this command.`);
656
+ return 2;
657
+ }
639
658
  const [st, w] = await apiGet(`${base}/v1/user/wallet`, token);
659
+ if (st === 403) {
660
+ console.error(`${BAD} this credential is not allowed to read the owner's treasury. ` +
661
+ `Sign in with \`pyyol login\` and try again.`);
662
+ return 1;
663
+ }
640
664
  if (st !== 200) {
641
665
  console.error(`${BAD} could not fetch wallet (${st}): ${JSON.stringify(w)}`);
642
666
  return 1;
@@ -894,9 +918,86 @@ async function cmdDoctor(a) {
894
918
  allOk = allOk && ok;
895
919
  console.log(` ${ok ? OK : BAD} ${name.padEnd(20)} ${detail}`);
896
920
  }
921
+ // Verified-tier readiness. Separate from the checks above because none of these is a failure of
922
+ // the agent: it will run, it just will not be RANKED. Conflating the two trains people to ignore
923
+ // a red mark that sometimes means nothing.
924
+ //
925
+ // Mirrors the Python SDK's section deliberately, down to the wording, because a developer must
926
+ // not get a different answer about their own eligibility depending on which SDK they installed.
927
+ await printVerifiedReadiness(base, c, cfg);
897
928
  console.log("\n" + (allOk ? "✓ ready — `pyyol dev` to practice, `pyyol play <arena>` to compete." : "fix the ✗ items above."));
898
929
  return allOk ? 0 : 1;
899
930
  }
931
+ /** True if the agent's entry file appears to send a system prompt, false if not, null if unknown.
932
+ *
933
+ * A shallow source scan, deliberately: it recognises Anthropic's `system=` and OpenAI's system /
934
+ * developer roles. Being approximate is fine because a wrong answer here is a HINT, not a
935
+ * decision — the authoritative value is the `scaffold` field on a real decision, which is what the
936
+ * unknown case points at.
937
+ *
938
+ * "developer" is included because it is OpenAI's newer name for the system role, and missing it
939
+ * would tell a correctly built agent it is ineligible — worse than saying nothing. */
940
+ export async function scaffoldHint(entry) {
941
+ if (!entry)
942
+ return null;
943
+ const path = String(entry).split(":")[0];
944
+ try {
945
+ const { readFileSync } = await import("node:fs");
946
+ const src = readFileSync(path, "utf8");
947
+ return ["system:", "system =", '"system"', "'system'", '"developer"', "'developer'"].some((n) => src.includes(n));
948
+ }
949
+ catch {
950
+ return null;
951
+ }
952
+ }
953
+ /** Report whether this agent will actually earn Verified, and if not, exactly why.
954
+ *
955
+ * Three things decide it and each fails silently on its own: routing (without it the platform sees
956
+ * no calls at all), a system prompt (without one the harness cannot be fingerprinted, so the agent
957
+ * is excluded from paired model comparison), and coverage (the share of decisions actually proven,
958
+ * against the threshold the verified tier requires).
959
+ *
960
+ * An agent can run perfectly and earn nothing. Nothing else in the toolchain says so, and learning
961
+ * it from an empty leaderboard row weeks later is the failure this prevents. */
962
+ export async function printVerifiedReadiness(base, c, cfg) {
963
+ const { gatewayBaseUrl } = await import("./instrument.js");
964
+ const { explain, ISSUE_NO_SYSTEM_PROMPT } = await import("./scaffold.js");
965
+ console.log("\nverified tier");
966
+ const routed = Boolean(gatewayBaseUrl("anthropic") || gatewayBaseUrl("openai"));
967
+ console.log(` ${routed ? OK : WARN} ${"gateway routing".padEnd(20)} ` +
968
+ (routed
969
+ ? "on — model calls are server-observed"
970
+ : "off — call pyyol.route(client) after pyyol.instrument(); without it no decision can be " +
971
+ "proven and this agent cannot appear on the model board"));
972
+ const hint = await scaffoldHint(cfg?.entry);
973
+ if (hint === null) {
974
+ console.log(` ${WARN} ${"system prompt".padEnd(20)} could not inspect the agent source; run \`pyyol dev\` ` +
975
+ "and check `scaffold` on a decision in the trace");
976
+ }
977
+ else if (hint) {
978
+ console.log(` ${OK} ${"system prompt".padEnd(20)} found — the harness can be fingerprinted, so this ` +
979
+ "agent is eligible for paired model comparison");
980
+ }
981
+ else {
982
+ // The shared explanation, never a paraphrase: the SDKs, the trace and this command have to give
983
+ // a developer the same sentence about one rule.
984
+ console.log(` ${WARN} ${"system prompt".padEnd(20)} none found. ${explain(ISSUE_NO_SYSTEM_PROMPT)}`);
985
+ }
986
+ if (base && c?.accessToken) {
987
+ const [st, body] = await apiGet(`${base}/v1/gw/coverage`, c.accessToken);
988
+ const b = body;
989
+ if (st === 200 && b?.decisions) {
990
+ const cov = Number(b.coverage ?? 0);
991
+ const mark = cov >= 0.9 ? OK : WARN;
992
+ console.log(` ${mark} ${"coverage".padEnd(20)} ${b.bound_decisions ?? 0}/${b.decisions} decisions ` +
993
+ `proven (${(cov * 100).toFixed(1)}%)` +
994
+ (cov >= 0.9 ? "" : " — below the 90% the verified tier requires"));
995
+ }
996
+ else if (st === 200) {
997
+ console.log(` ${WARN} ${"coverage".padEnd(20)} no decisions recorded yet — play a match first`);
998
+ }
999
+ }
1000
+ }
900
1001
  async function cmdUpdate() {
901
1002
  console.log(`pyyol ${SDK_VERSION}`);
902
1003
  try {
package/dist/index.d.ts CHANGED
@@ -27,4 +27,6 @@ export { instrument, uninstrument, recordResponse, extractUsage, patchPrototype
27
27
  export { route, enableGateway, disableGateway, gatewayBaseUrl, gatewayHeaders } from "./instrument.js";
28
28
  export type { ExtractedUsage } from "./instrument.js";
29
29
  export { estimateCost, rateFor, isKnown, canonical, PRICING_VERSION } from "./pricing.js";
30
+ export { moveTool, moveToolChoice, moveToolName, moveFromResponse, boundMove, boundPlan, canonPlan, PLAN_KEY, MAX_SPAN_ROUNDS, canonMove, canonGoofspiel, canonMafia, canonMonopoly, NO_TARGET, TOOL_GOOFSPIEL, TOOL_MAFIA, TOOL_MONOPOLY, GAME_GOOFSPIEL, GAME_MAFIA, GAME_MONOPOLY, } from "./movetools.js";
30
31
  export type { Rate, CostArgs } from "./pricing.js";
32
+ export { fingerprint as scaffoldFingerprint, fromRequest as scaffoldFromRequest, eligibleForPairing as scaffoldEligibleForPairing, SCAFFOLD_VERSION, } from "./scaffold.js";
package/dist/index.js CHANGED
@@ -21,3 +21,14 @@ export { Tracer, Span, currentSpan, matchTraceId, currentUsage, UsageAccumulator
21
21
  export { instrument, uninstrument, recordResponse, extractUsage, patchPrototype } from "./instrument.js";
22
22
  export { route, enableGateway, disableGateway, gatewayBaseUrl, gatewayHeaders } from "./instrument.js";
23
23
  export { estimateCost, rateFor, isKnown, canonical, PRICING_VERSION } from "./pricing.js";
24
+ // Structured move tools: how an agent proves its MODEL chose the move it played. Routing
25
+ // through the gateway proves a call happened for a turn; a tool call is what proves the
26
+ // model's answer became the move. See src/movetools.ts.
27
+ export { moveTool, moveToolChoice, moveToolName, moveFromResponse, boundMove,
28
+ // Range bindings: one completion that decided several rounds. Coverage counts DECISIONS a
29
+ // model made, not calls, so batching no longer costs an agent its verified share.
30
+ boundPlan, canonPlan, PLAN_KEY, MAX_SPAN_ROUNDS, canonMove, canonGoofspiel, canonMafia, canonMonopoly, NO_TARGET, TOOL_GOOFSPIEL, TOOL_MAFIA, TOOL_MONOPOLY, GAME_GOOFSPIEL, GAME_MAFIA, GAME_MONOPOLY, } from "./movetools.js";
31
+ // Scaffold fingerprinting: the harness identity that makes a paired model comparison
32
+ // possible (same scaffold, different model). Exported so a developer can print their own
33
+ // fingerprint and confirm it is stable before relying on it.
34
+ export { fingerprint as scaffoldFingerprint, fromRequest as scaffoldFromRequest, eligibleForPairing as scaffoldEligibleForPairing, SCAFFOLD_VERSION, } from "./scaffold.js";
@@ -2,10 +2,26 @@ type Any = any;
2
2
  /** Enable gateway routing (called by the runtime in ranked mode; safe in tests). */
3
3
  export declare function enableGateway(agentKey: string, baseUrl: string): void;
4
4
  export declare function disableGateway(): void;
5
- /** The baseURL a provider client should point at, or "" if routing is off/unknown. */
5
+ /**
6
+ * The baseURL a provider client should point at, or "" when routing is off or the provider
7
+ * cannot be routed.
8
+ *
9
+ * Returns "" for a LOCAL/self-hosted provider, deliberately: the gateway runs on Pyyol's side
10
+ * and cannot reach a model server on the developer's own machine, so pointing a client at it
11
+ * would break every call. That play is unverified — and also free, so no cost attribution is
12
+ * lost either.
13
+ */
6
14
  export declare function gatewayBaseUrl(provider: string): string;
7
15
  /** The X-Pyyol-* identity headers for the current turn (empty if routing off). */
8
16
  export declare function gatewayHeaders(): Record<string, string>;
17
+ /** The provider for one instrumented call.
18
+ *
19
+ * `patchedAs` is the SDK we wrapped (which wire format this is). The client's baseURL
20
+ * is consulted first and wins. A baseURL pointing at the PYYOL GATEWAY is not used for
21
+ * attribution — it says the call was proxied, not who served it — so the upstream is
22
+ * recovered by PARSING the gateway path (/gw/<slug>[/v1]) rather than matching a table, so a
23
+ * provider added tomorrow is attributed correctly without touching this. */
24
+ export declare function resolveCallProvider(resource: Any, patchedAs: string): string;
9
25
  /** Point a provider client at the Pyyol Gateway (sets its baseURL). Explicit, robust
10
26
  * opt-in that operates on the given instance. Returns the client. No-op when routing
11
27
  * is off or the provider can't be determined. */
@@ -15,12 +31,26 @@ export interface ExtractedUsage {
15
31
  provider: string;
16
32
  promptTokens: number;
17
33
  completionTokens: number;
34
+ /** Prompt-cache READ tokens. */
18
35
  cachedTokens: number;
36
+ /** Prompt-cache WRITE/creation tokens. Billed at 1.25x input on Anthropic, so an
37
+ * agent's most expensive tokens were previously recorded as zero. */
38
+ cachedWriteTokens: number;
19
39
  reasoningTokens: number;
20
40
  }
21
- /** Pull normalized usage from a provider response, or null if it has none.
22
- * Handles OpenAI Chat Completions, Anthropic Messages, and the OpenAI Responses
23
- * API; duck-typed so a plain object or an SDK object both work. */
41
+ /**
42
+ * Pull normalized usage from ANY provider response, or null if it carries none.
43
+ *
44
+ * Normalizes onto ONE convention: promptTokens is the total billable input, with cache reads and
45
+ * writes as SUBSETS of it. Providers genuinely disagree and the disagreement is silent, so the rule
46
+ * follows the WORD USED rather than a vendor list:
47
+ *
48
+ * - a PROMPT-family key names the whole prompt, so cache is already inside it
49
+ * - an INPUT-family key names fresh input, so cache is billed on top
50
+ *
51
+ * A reported total cross-checks the additive case, so a provider using "input" for a
52
+ * cache-inclusive total is corrected by its own arithmetic instead of being over-counted.
53
+ */
24
54
  export declare function extractUsage(resp: Any): ExtractedUsage | null;
25
55
  /** Record usage from a provider response: compute cost, add to the turn
26
56
  * accumulator, and emit a Lens model_call span. Returns the extracted usage (or
@@ -31,7 +61,7 @@ export declare function recordResponse(resp: Any, o?: {
31
61
  }): ExtractedUsage | null;
32
62
  /** @internal Wrap `proto[method]` so its resolved return value is recorded.
33
63
  * Idempotent and fully guarded. Exported for tests. */
34
- export declare function patchPrototype(proto: Any, method: string, provider: string): boolean;
64
+ export declare function patchPrototype(proto: Any, method: string, provider: string, endpoint?: string): boolean;
35
65
  /** Auto-capture LLM usage from installed providers. Pass e.g. `["openai"]` to limit
36
66
  * which are patched; default patches all supported providers that are installed.
37
67
  * Returns the list actually instrumented. Safe to call more than once. */