@arnilo/prism 0.8.0 → 0.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/CHANGELOG.md +62 -1
- package/README.md +13 -12
- package/dist/agent-approval.d.ts +17 -2
- package/dist/agent-approval.js +15 -6
- package/dist/agent-event-source.d.ts +9 -1
- package/dist/agent-event-source.js +10 -3
- package/dist/agent-loops.js +7 -4
- package/dist/agent-run-lifecycle.d.ts +15 -1
- package/dist/agent-run-lifecycle.js +82 -11
- package/dist/agent-run-state.d.ts +47 -6
- package/dist/agent-run-state.js +154 -6
- package/dist/agent-session/event-subscriber.d.ts +2 -0
- package/dist/agent-session/event-subscriber.js +3 -0
- package/dist/agent-session/helpers.js +14 -0
- package/dist/agent-session/session/assemble.js +281 -32
- package/dist/agent-session/session/persist.d.ts +11 -0
- package/dist/agent-session/session/persist.js +48 -16
- package/dist/agent-session/session/provider-round.d.ts +14 -4
- package/dist/agent-session/session/provider-round.js +226 -19
- package/dist/agent-session/session/tool-round.d.ts +2 -2
- package/dist/agent-session/session/tool-round.js +78 -6
- package/dist/agent-session/session/types.d.ts +44 -3
- package/dist/agent-session/session.d.ts +100 -5
- package/dist/agent-session/session.js +224 -13
- package/dist/attention-compiler.d.ts +51 -2
- package/dist/attention-compiler.js +282 -21
- package/dist/cache-helpers.d.ts +4 -2
- package/dist/cache-helpers.js +8 -6
- package/dist/checkpoint-restore.d.ts +45 -0
- package/dist/checkpoint-restore.js +54 -0
- package/dist/context-budget.d.ts +13 -1
- package/dist/context-budget.js +57 -4
- package/dist/contracts-core/agent.d.ts +52 -1
- package/dist/contracts-core/attention.d.ts +95 -0
- package/dist/contracts-core/content.d.ts +10 -0
- package/dist/contracts-core/extensions.d.ts +3 -0
- package/dist/contracts-core/guardrail-packs.d.ts +46 -0
- package/dist/contracts-core/guardrail-packs.js +2 -0
- package/dist/contracts-core/loop.d.ts +36 -0
- package/dist/contracts-core/provider.d.ts +30 -0
- package/dist/contracts-core/run-limits.d.ts +29 -1
- package/dist/contracts-core/session.d.ts +23 -5
- package/dist/contracts-core/session.js +21 -2
- package/dist/contracts-core/usage.d.ts +40 -0
- package/dist/contracts-core/usage.js +8 -0
- package/dist/contracts-core.d.ts +2 -0
- package/dist/contracts-core.js +2 -0
- package/dist/contracts-protocol.d.ts +81 -5
- package/dist/contracts-run-state.d.ts +91 -2
- package/dist/contributions.d.ts +2 -1
- package/dist/contributions.js +1 -0
- package/dist/extensions.d.ts +15 -1
- package/dist/extensions.js +68 -0
- package/dist/guardrail-packs/coding-standard.d.ts +3 -0
- package/dist/guardrail-packs/coding-standard.js +63 -0
- package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
- package/dist/guardrail-packs/destructive-commands.js +46 -0
- package/dist/guardrail-packs/errors.d.ts +7 -0
- package/dist/guardrail-packs/errors.js +9 -0
- package/dist/guardrail-packs/index.d.ts +4 -0
- package/dist/guardrail-packs/index.js +15 -0
- package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
- package/dist/guardrail-packs/secrets-hygiene.js +23 -0
- package/dist/guardrail-packs/types.d.ts +26 -0
- package/dist/guardrail-packs/types.js +2 -0
- package/dist/guardrail-packs/validation-respect.d.ts +3 -0
- package/dist/guardrail-packs/validation-respect.js +69 -0
- package/dist/guardrails.d.ts +61 -1
- package/dist/guardrails.js +377 -0
- package/dist/index.d.ts +16 -11
- package/dist/index.js +10 -7
- package/dist/input.d.ts +8 -1
- package/dist/input.js +68 -6
- package/dist/middleware.d.ts +37 -2
- package/dist/middleware.js +41 -0
- package/dist/node/session-store-jsonl.js +18 -3
- package/dist/observability.js +6 -0
- package/dist/provider-events.d.ts +8 -2
- package/dist/provider-events.js +60 -2
- package/dist/providers/openai-compatible.js +6 -3
- package/dist/run-bundle.d.ts +6 -1
- package/dist/run-bundle.js +5 -1
- package/dist/run-limits.d.ts +11 -1
- package/dist/run-limits.js +59 -0
- package/dist/session-stores.d.ts +12 -1
- package/dist/session-stores.js +21 -4
- package/dist/testing/agent-event-source-conformance.js +41 -2
- package/dist/testing/prefix-stability-conformance.d.ts +59 -0
- package/dist/testing/prefix-stability-conformance.js +172 -0
- package/dist/testing/session-store-conformance.d.ts +3 -2
- package/dist/testing/session-store-conformance.js +48 -0
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +21 -6
- package/dist/usage-estimation.d.ts +29 -0
- package/dist/usage-estimation.js +79 -0
- package/docs/agent-events.md +75 -4
- package/docs/agent-session-runtime.md +10 -6
- package/docs/attention-compiler.md +89 -8
- package/docs/caveman.md +1 -1
- package/docs/coding-agent-tools.md +1 -1
- package/docs/compaction-and-retry.md +1 -1
- package/docs/compaction-llm.md +2 -0
- package/docs/compaction-observational-memory.md +54 -7
- package/docs/durable-runs.md +46 -3
- package/docs/embeddings.md +9 -0
- package/docs/evaluations.md +5 -0
- package/docs/execution-timeline.md +79 -1
- package/docs/extensions.md +20 -3
- package/docs/guardrails.md +50 -4
- package/docs/hooks.md +282 -0
- package/docs/index.md +37 -15
- package/docs/input-and-prompt-assembly.md +4 -4
- package/docs/instruction-injection.md +1 -0
- package/docs/knowledge-sync.md +4 -0
- package/docs/live-testing.md +3 -1
- package/docs/memory-fabric.md +28 -0
- package/docs/middleware-hooks.md +90 -4
- package/docs/migrate-to-0.9.md +210 -0
- package/docs/migration.md +26 -0
- package/docs/multi-agent-patterns.md +25 -2
- package/docs/node-jsonl-session-store.md +7 -1
- package/docs/observability.md +7 -3
- package/docs/options-index.md +4 -1
- package/docs/policy-and-audit.md +26 -1
- package/docs/prefix-stability-conformance.md +143 -0
- package/docs/provider-caching.md +4 -4
- package/docs/provider-conformance.md +16 -0
- package/docs/provider-packages.md +20 -20
- package/docs/public-contracts.md +3 -2
- package/docs/rag.md +188 -3
- package/docs/release-and-install.md +45 -40
- package/docs/runs-and-usage.md +56 -10
- package/docs/scoped-agent-memory.md +270 -0
- package/docs/scoped-memory.md +138 -0
- package/docs/session-store-conformance.md +1 -2
- package/docs/session-stores.md +17 -17
- package/docs/supervisors.md +32 -12
- package/docs/tools.md +18 -1
- package/docs/wiki.md +4 -2
- package/docs/workflows.md +5 -0
- package/package.json +8 -2
|
@@ -28,14 +28,53 @@ export async function assertAgentEventSourceConforms(factory) {
|
|
|
28
28
|
const final = await source.page({ ...input, after: secondPage.items[0].cursor, limit: 10 });
|
|
29
29
|
equal(final.items.at(-1)?.record.id, terminal.id, "terminal page must include its terminal event");
|
|
30
30
|
equal(final.terminal, true, "terminal event must close only after prior events are delivered");
|
|
31
|
+
// A limit death delivers its attribution before the run's outcome: the breach and budget records
|
|
32
|
+
// are not terminal, the `error` that follows them is.
|
|
33
|
+
const breached = { ...input, runId: "run-limit" };
|
|
34
|
+
await source.append(event("event-limit-before", "turn_started", breached, "2026-01-01T00:00:04.000Z"));
|
|
35
|
+
const beforeLimitItem = (await source.page({ ...breached, limit: 1 })).items[0];
|
|
36
|
+
if (!beforeLimitItem)
|
|
37
|
+
throw new Error("limit-death precondition missing");
|
|
38
|
+
const beforeLimit = beforeLimitItem.cursor;
|
|
39
|
+
await source.append(event("event-limit", "run_limit_exceeded", breached, "2026-01-01T00:00:05.000Z"));
|
|
40
|
+
await source.append(event("event-attr", "budget_exhausted", breached, "2026-01-01T00:00:06.000Z"));
|
|
41
|
+
const breachPage = await source.page({ ...breached, after: beforeLimit, limit: 10 });
|
|
42
|
+
equal(breachPage.items.length, 2, "the breach and its attribution must page together");
|
|
43
|
+
equal(breachPage.terminal, false, "a limit breach must not close the stream; the run's error follows");
|
|
44
|
+
await source.append(event("event-limit-error", "error", breached, "2026-01-01T00:00:07.000Z"));
|
|
45
|
+
const deadPage = await source.page({ ...breached, after: beforeLimit, limit: 10 });
|
|
46
|
+
equal(deadPage.terminal, true, "the error after a limit breach closes the stream");
|
|
47
|
+
const death = source.subscribe(breached)[Symbol.asyncIterator]();
|
|
48
|
+
const delivered = [];
|
|
49
|
+
for (;;) {
|
|
50
|
+
const next = await death.next();
|
|
51
|
+
if (next.done)
|
|
52
|
+
break;
|
|
53
|
+
delivered.push(next.value.record.type);
|
|
54
|
+
}
|
|
55
|
+
equal(delivered.join(","), "turn_started,run_limit_exceeded,budget_exhausted,error", "a subscriber must read the breach and its attribution before the stream ends");
|
|
31
56
|
await rejects(() => source.page({ ...input, ownership: { ...ownership, tenantId: "tenant-b" }, after: page.nextCursor }), "foreign cursor must fail closed");
|
|
32
57
|
await rejects(() => source.append({ ...event("event-unredacted", "turn_started", input), redacted: false }), "unredacted append must fail");
|
|
33
58
|
await rejects(() => source.page({ ...input, limit: 0 }), "invalid page limit must fail");
|
|
34
59
|
}
|
|
35
60
|
function event(id, type, input, timestamp = "2026-01-01T00:00:00.000Z") {
|
|
61
|
+
const scoped = { sessionId: input.sessionId, runId: input.runId };
|
|
36
62
|
const event = type === "turn_started"
|
|
37
|
-
? { type,
|
|
38
|
-
:
|
|
63
|
+
? { type, ...scoped, turn: 1 }
|
|
64
|
+
: type === "run_limit_exceeded"
|
|
65
|
+
? { type, ...scoped, breach: { limit: "maxTurns", maximum: 1, observed: 2 } }
|
|
66
|
+
: type === "budget_exhausted"
|
|
67
|
+
? {
|
|
68
|
+
type,
|
|
69
|
+
...scoped,
|
|
70
|
+
limit: "maxTurns",
|
|
71
|
+
consumed: { turns: 2, inputTokens: 0, providerAttempts: 1, requestBytes: 0 },
|
|
72
|
+
closestOtherAxes: [],
|
|
73
|
+
recentToolCalls: [],
|
|
74
|
+
}
|
|
75
|
+
: type === "error"
|
|
76
|
+
? { type, ...scoped, error: { message: "run limit exceeded" } }
|
|
77
|
+
: { type, ...scoped };
|
|
39
78
|
return { id, ...input.ownership, sessionId: input.sessionId, runId: input.runId, type, timestamp, event, redacted: true };
|
|
40
79
|
}
|
|
41
80
|
function equal(actual, expected, message) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AgentConfig, Skill } from "../contracts.js";
|
|
2
|
+
export interface PrefixStabilityConformanceOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The host's own agent config, minus `provider`, `providerSource`, and `skills`:
|
|
5
|
+
* the runner installs its fixture provider and fixture skill registry so the
|
|
6
|
+
* scenario is deterministic and comparable across hosts.
|
|
7
|
+
*/
|
|
8
|
+
readonly host: Omit<AgentConfig, "provider" | "providerSource" | "skills">;
|
|
9
|
+
/** Two distinct skills: the fixture loads `[0]` on the first turn and `[1]` on the second. */
|
|
10
|
+
readonly skills: readonly [Skill, Skill];
|
|
11
|
+
/** Minimum shared byte-prefix fraction between consecutive requests. Default `0.95`. */
|
|
12
|
+
readonly minContinuity?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Which fraction gates the run: `"providerPrefix"` (default, today's behavior) asserts the
|
|
15
|
+
* provider-visible prefix; `"cacheablePrefix"` asserts the same measurement with tail segments
|
|
16
|
+
* removed, so a body-heavy or eager host is not failed for the tail it deliberately re-sends.
|
|
17
|
+
*/
|
|
18
|
+
readonly assertOn?: "providerPrefix" | "cacheablePrefix";
|
|
19
|
+
/** Turn inputs; defaults are fixed strings so runs are comparable across hosts. */
|
|
20
|
+
readonly inputs?: readonly [string, string];
|
|
21
|
+
/**
|
|
22
|
+
* How many request pairs may break below `minContinuity` (default `0`, today's behavior). Use
|
|
23
|
+
* `1` for an assembly that folds or evicts exactly one boundary — an attention-compiler fold,
|
|
24
|
+
* a compaction, a budget eviction. More resets than declared fail, and fewer fail too: the
|
|
25
|
+
* fixture was supposed to invalidate the prefix, so a run that never did cannot pass vacuously.
|
|
26
|
+
*/
|
|
27
|
+
readonly allowedResets?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface PrefixStabilityConformanceResult {
|
|
30
|
+
/** Provider requests captured by the fixture (two per turn: skill load, then completion). */
|
|
31
|
+
readonly requests: number;
|
|
32
|
+
/** Lowest shared-prefix fraction observed across consecutive captured requests. */
|
|
33
|
+
readonly minContinuity: number;
|
|
34
|
+
/**
|
|
35
|
+
* The same lowest fraction with the session's tail segments removed from both requests of each
|
|
36
|
+
* pair — the provider-visible prefix the cache can actually keep paying for. Equals
|
|
37
|
+
* `minContinuity` when no captured request carried a tail segment.
|
|
38
|
+
*/
|
|
39
|
+
readonly cacheableContinuity: number;
|
|
40
|
+
/**
|
|
41
|
+
* 1-based indexes of the captured requests whose asserted prefix broke below `minContinuity`
|
|
42
|
+
* (the later request of each pair), in ascending order — where the assembly invalidated the
|
|
43
|
+
* prefix instead of appending. Empty when every gap stayed above the minimum.
|
|
44
|
+
*/
|
|
45
|
+
readonly resets: readonly number[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Drive a real session through two staggered skill loads and assert that each
|
|
49
|
+
* provider request keeps a byte-identical leading prefix (messages **and** tool
|
|
50
|
+
* schemas) with its predecessor. Progressive disclosure appends a loaded body
|
|
51
|
+
* after the stable prefix, so the shared prefix stays intact; a host that
|
|
52
|
+
* rewrites the context block, the skill catalog, or any leading message per
|
|
53
|
+
* request fails with the offending request pair and the measured fraction.
|
|
54
|
+
* Reports both the provider-visible fraction and the same fraction with the
|
|
55
|
+
* session's tail segments removed; `assertOn` picks which one gates the run.
|
|
56
|
+
* A gap below the minimum is collected as a reset instead of failing in the
|
|
57
|
+
* loop, so `allowedResets` can permit the one boundary an assembly folds at.
|
|
58
|
+
*/
|
|
59
|
+
export declare function runPrefixStabilityConformance(options: PrefixStabilityConformanceOptions): Promise<PrefixStabilityConformanceResult>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// ponytail: dependency-free conformance runner for prompt-cache prefix stability.
|
|
2
|
+
// The runner owns the fixture provider (network-free, deterministic) and fixture
|
|
3
|
+
// skills; everything else in `host` is the caller's production assembly — system
|
|
4
|
+
// prompt, context providers, prompt builder, middleware, disclosure settings.
|
|
5
|
+
// Throws plain Error; no test runner, no network, no credentials.
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { createAgent } from "../agent-session/create-agent.js";
|
|
8
|
+
import { providerDone, providerThinkingDelta, toolCallContent } from "../provider-events.js";
|
|
9
|
+
import { createLoadSkillTool } from "../skill-load.js";
|
|
10
|
+
import { createSkillRegistry } from "../skills.js";
|
|
11
|
+
/**
|
|
12
|
+
* Drive a real session through two staggered skill loads and assert that each
|
|
13
|
+
* provider request keeps a byte-identical leading prefix (messages **and** tool
|
|
14
|
+
* schemas) with its predecessor. Progressive disclosure appends a loaded body
|
|
15
|
+
* after the stable prefix, so the shared prefix stays intact; a host that
|
|
16
|
+
* rewrites the context block, the skill catalog, or any leading message per
|
|
17
|
+
* request fails with the offending request pair and the measured fraction.
|
|
18
|
+
* Reports both the provider-visible fraction and the same fraction with the
|
|
19
|
+
* session's tail segments removed; `assertOn` picks which one gates the run.
|
|
20
|
+
* A gap below the minimum is collected as a reset instead of failing in the
|
|
21
|
+
* loop, so `allowedResets` can permit the one boundary an assembly folds at.
|
|
22
|
+
*/
|
|
23
|
+
export async function runPrefixStabilityConformance(options) {
|
|
24
|
+
const { host, skills } = options;
|
|
25
|
+
const minContinuity = options.minContinuity ?? 0.95;
|
|
26
|
+
const [first, second] = skills;
|
|
27
|
+
assert.notEqual(first.name, second.name, "prefix stability conformance needs two distinct skills");
|
|
28
|
+
const bodies = [];
|
|
29
|
+
for (const skill of skills) {
|
|
30
|
+
const instructions = skill.instructions;
|
|
31
|
+
assert.ok(typeof instructions === "string" && instructions.length > 0, `prefix stability conformance skill ${skill.name} needs non-empty instructions`);
|
|
32
|
+
bodies.push(instructions);
|
|
33
|
+
}
|
|
34
|
+
const requests = [];
|
|
35
|
+
const registry = createSkillRegistry([...skills]);
|
|
36
|
+
const hostTools = host.tools && "list" in host.tools ? host.tools.list() : (host.tools ?? []);
|
|
37
|
+
const agent = createAgent({
|
|
38
|
+
...host,
|
|
39
|
+
skills: registry,
|
|
40
|
+
tools: [...hostTools, createLoadSkillTool({ registry })],
|
|
41
|
+
provider: fixtureProvider(requests, [first.name, second.name], host.attentionCompiler === true || typeof host.attentionCompiler === "object"),
|
|
42
|
+
});
|
|
43
|
+
const session = agent.createSession();
|
|
44
|
+
const [firstInput, secondInput] = options.inputs ?? ["Prefix stability turn one", "Prefix stability turn two"];
|
|
45
|
+
const runOptions = { activeSkills: [first.name, second.name], limits: { maxToolRounds: 1 } };
|
|
46
|
+
await session.run(firstInput, runOptions);
|
|
47
|
+
await session.run(secondInput, runOptions);
|
|
48
|
+
assert.equal(requests.length, 4, `prefix stability conformance expected 4 provider requests (2 per staggered turn), captured ${requests.length}`);
|
|
49
|
+
// The session's own map holds the exact `Message` objects `appendTailSegment` allocated, so the
|
|
50
|
+
// classification is exact rather than a heuristic over host-authored content. (`tailSegments` is
|
|
51
|
+
// runtime-session state, not part of the public `AgentSession` contract, hence the narrow above.)
|
|
52
|
+
const isTail = tailClassifier(session.tailSegments);
|
|
53
|
+
const captured = requests.map((request) => measureRequest(request, isTail));
|
|
54
|
+
// Guard against a vacuous pass: both bodies must have been disclosed by the end.
|
|
55
|
+
const last = captured.at(-1)?.providerPrefix ?? "";
|
|
56
|
+
for (const [index, skill] of skills.entries()) {
|
|
57
|
+
assert.ok(last.includes(bodies[index] ?? ""), `prefix stability conformance: skill ${skill.name} body never reached the provider request — progressive disclosure did not expand it`);
|
|
58
|
+
}
|
|
59
|
+
const assertOn = options.assertOn ?? "providerPrefix";
|
|
60
|
+
const allowedResets = options.allowedResets ?? 0;
|
|
61
|
+
assert.ok(Number.isSafeInteger(allowedResets) && allowedResets >= 0, "prefix stability conformance allowedResets must be a non-negative safe integer");
|
|
62
|
+
let observed = 1;
|
|
63
|
+
let cacheableObserved = 1;
|
|
64
|
+
let previous = captured.at(0) ?? { providerPrefix: "", cacheablePrefix: "" };
|
|
65
|
+
// Collect every gap first: an allowed reset must not be hidden by a later assert, and the
|
|
66
|
+
// vacuity check needs the whole list to prove the fixture folded exactly as declared.
|
|
67
|
+
const gaps = [];
|
|
68
|
+
for (let index = 1; index < captured.length; index += 1) {
|
|
69
|
+
const next = captured[index] ?? previous;
|
|
70
|
+
const fraction = sharedPrefixFraction(previous.providerPrefix, next.providerPrefix);
|
|
71
|
+
const cacheableFraction = sharedPrefixFraction(previous.cacheablePrefix, next.cacheablePrefix);
|
|
72
|
+
observed = Math.min(observed, fraction);
|
|
73
|
+
cacheableObserved = Math.min(cacheableObserved, cacheableFraction);
|
|
74
|
+
const measured = assertOn === "cacheablePrefix" ? cacheableFraction : fraction;
|
|
75
|
+
if (measured < minContinuity)
|
|
76
|
+
gaps.push({ request: index + 1, fraction, cacheableFraction });
|
|
77
|
+
previous = next;
|
|
78
|
+
}
|
|
79
|
+
const resets = gaps.map((gap) => gap.request);
|
|
80
|
+
const measuredLabel = assertOn === "cacheablePrefix" ? "previous cacheable prefix (tail segments excluded)" : "previous provider prefix";
|
|
81
|
+
const minimum = (minContinuity * 100).toFixed(1);
|
|
82
|
+
const observedResets = `resets ${formatResets(resets)} of ${captured.length - 1} request pairs`;
|
|
83
|
+
const firstGap = gaps[0];
|
|
84
|
+
if (firstGap !== undefined && gaps.length > allowedResets) {
|
|
85
|
+
const measured = assertOn === "cacheablePrefix" ? firstGap.cacheableFraction : firstGap.fraction;
|
|
86
|
+
assert.fail(`prefix stability conformance: request ${firstGap.request - 1} → ${firstGap.request} kept ${(measured * 100).toFixed(1)}% of the ${measuredLabel} ` +
|
|
87
|
+
`(minimum ${minimum}%), and ${gaps.length} pair(s) broke below it (${observedResets}, allowedResets ${allowedResets}). ` +
|
|
88
|
+
"Late skill bodies must append after the stable prefix; recomposed context, an in-place skill-catalog rewrite, or any leading-message mutation invalidates it. " +
|
|
89
|
+
"Pass allowedResets for the fold, compaction, or eviction the assembly performs per run, or fix the assembly so every other gap stays byte-stable.");
|
|
90
|
+
}
|
|
91
|
+
if (gaps.length < allowedResets) {
|
|
92
|
+
assert.fail(`prefix stability conformance: allowedResets is ${allowedResets} but only ${gaps.length} pair(s) broke below the minimum (${minimum}% of the ${measuredLabel}); ${observedResets}. ` +
|
|
93
|
+
"The fixture was supposed to invalidate the prefix at those boundaries — drop allowedResets for an append-only assembly, or check the fold trigger or eviction condition actually fired.");
|
|
94
|
+
}
|
|
95
|
+
return { requests: captured.length, minContinuity: observed, cacheableContinuity: cacheableObserved, resets };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Deterministic reasoning block the fixture provider emits before each skill load when the host
|
|
99
|
+
* runs an attention compiler. Sized to be a real fraction of the request so the compiler's
|
|
100
|
+
* thinking stage (`thinkingKeepTurns`) has something to strip and the resulting fold is visible
|
|
101
|
+
* in the measured prefix.
|
|
102
|
+
*/
|
|
103
|
+
const FIXTURE_THINKING = "Prefix-stability fixture reasoning: the harness measures a byte-shared provider prefix, so this block exists only to give the attention-compiler thinking stage deterministic content to strip. ".repeat(17);
|
|
104
|
+
/**
|
|
105
|
+
* Fixture provider: turn 1 loads `skillNames[0]`, turn 2 loads `skillNames[1]`, everything else
|
|
106
|
+
* completes. With `reasoning` (the host runs an attention compiler) each skill-load round also
|
|
107
|
+
* carries a thinking block, so the compiler's thinking stage has real content to fold.
|
|
108
|
+
*/
|
|
109
|
+
function fixtureProvider(requests, skillNames, reasoning) {
|
|
110
|
+
let call = 0;
|
|
111
|
+
return {
|
|
112
|
+
id: "prefix-stability-fixture",
|
|
113
|
+
async *generate(request) {
|
|
114
|
+
requests.push(request);
|
|
115
|
+
const index = call;
|
|
116
|
+
call += 1;
|
|
117
|
+
const skillName = skillNames[index >> 1];
|
|
118
|
+
if (index % 2 === 0 && skillName !== undefined) {
|
|
119
|
+
if (reasoning)
|
|
120
|
+
yield providerThinkingDelta(FIXTURE_THINKING);
|
|
121
|
+
yield { type: "tool_call", call: toolCallContent(`prefix-stability-${index}`, "load_skill", { name: skillName }) };
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
yield providerDone();
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Classifies a captured message as a tail segment: by object identity first (the default builder
|
|
130
|
+
* passes the session's own `Message` objects through), then by serialized equality for builders
|
|
131
|
+
* that clone messages. Takes the pre-serialized fragment so each message is serialized once.
|
|
132
|
+
*/
|
|
133
|
+
function tailClassifier(tailSegments) {
|
|
134
|
+
const identities = new Set(tailSegments.values());
|
|
135
|
+
const values = new Set([...identities].map((message) => JSON.stringify(message)));
|
|
136
|
+
return (message, fragment) => identities.has(message) || values.has(fragment);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Provider-visible payload only — messages plus the tool schema fields sent on the wire — measured
|
|
140
|
+
* twice: whole, and with tail segments dropped. One JSON fragment per message/tool so a structural
|
|
141
|
+
* array boundary never reads as a byte divergence: an appended message list stays an exact prefix
|
|
142
|
+
* of the next request.
|
|
143
|
+
*/
|
|
144
|
+
function measureRequest(request, isTail) {
|
|
145
|
+
const toolParts = (request.tools ?? []).map((tool) => JSON.stringify({ name: tool.name, description: tool.description, parameters: tool.parameters }));
|
|
146
|
+
const providerParts = [...toolParts];
|
|
147
|
+
const cacheableParts = [...toolParts];
|
|
148
|
+
for (const message of request.messages) {
|
|
149
|
+
const fragment = JSON.stringify(message);
|
|
150
|
+
providerParts.push(fragment);
|
|
151
|
+
if (!isTail(message, fragment))
|
|
152
|
+
cacheableParts.push(fragment);
|
|
153
|
+
}
|
|
154
|
+
return { providerPrefix: providerParts.join("\n"), cacheablePrefix: cacheableParts.join("\n") };
|
|
155
|
+
}
|
|
156
|
+
/** Bracket form for reset lists, e.g. `[3]` or `[3, 4]`. */
|
|
157
|
+
function formatResets(resets) {
|
|
158
|
+
return `[${resets.join(", ")}]`;
|
|
159
|
+
}
|
|
160
|
+
/** Byte-shared prefix as a fraction of the previous request, so a shrink is a cache miss. */
|
|
161
|
+
function sharedPrefixFraction(previous, next) {
|
|
162
|
+
const before = Buffer.from(previous, "utf8");
|
|
163
|
+
const after = Buffer.from(next, "utf8");
|
|
164
|
+
if (before.length === 0)
|
|
165
|
+
return 1;
|
|
166
|
+
const limit = Math.min(before.length, after.length);
|
|
167
|
+
let shared = 0;
|
|
168
|
+
while (shared < limit && before[shared] === after[shared])
|
|
169
|
+
shared += 1;
|
|
170
|
+
return shared / before.length;
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=prefix-stability-conformance.js.map
|
|
@@ -11,8 +11,9 @@ export interface SessionStoreConformanceOptions {
|
|
|
11
11
|
*/
|
|
12
12
|
readonly exerciseReadBranchPath?: boolean;
|
|
13
13
|
/**
|
|
14
|
-
* When true, exercises optional `searchSessions
|
|
15
|
-
*
|
|
14
|
+
* When true, exercises optional `searchSessions`: invalid limit/query/kind rejection via
|
|
15
|
+
* `resolveSessionSearchQuery` semantics, empty page, limit cap, a written-message query
|
|
16
|
+
* round-trip (`entryId`/`runId`/`turn`/`snippet`), the `kind` filter, and ownership bounds.
|
|
16
17
|
* Skipped when the store does not implement `searchSessions`.
|
|
17
18
|
*/
|
|
18
19
|
readonly exerciseSearchSessions?: boolean;
|
|
@@ -113,6 +113,54 @@ async function assertSessionStoreSearchSessions(store) {
|
|
|
113
113
|
await reject(() => search({ limit: Number.NaN }), (error) => error instanceof TypeError, "searchSessions must reject NaN limit");
|
|
114
114
|
await reject(() => search({ limit: HARD_MAX_SESSION_SEARCH_LIMIT + 1 }), (error) => error instanceof TypeError, "searchSessions must reject oversize limit");
|
|
115
115
|
await reject(() => search({ query: "x".repeat(HARD_MAX_SESSION_SEARCH_QUERY_BYTES + 1) }), (error) => error instanceof TypeError, "searchSessions must reject oversize query string");
|
|
116
|
+
await reject(() => search({ kind: "not-an-entry-kind" }), (error) => error instanceof TypeError, "searchSessions must reject an unknown kind");
|
|
117
|
+
// Query round-trip: a written message must be findable and the hit must point at it.
|
|
118
|
+
const searchSessionId = "conformance-search";
|
|
119
|
+
const token = "zzconformancesearchtoken";
|
|
120
|
+
const matchedEntry = {
|
|
121
|
+
id: "conformance-search-entry",
|
|
122
|
+
sessionId: searchSessionId,
|
|
123
|
+
timestamp: "2026-01-01T00:00:05.000Z",
|
|
124
|
+
kind: "message",
|
|
125
|
+
runId: "conformance-run",
|
|
126
|
+
message: { role: "user", content: [{ type: "text", text: `${token} body text` }] },
|
|
127
|
+
};
|
|
128
|
+
await store.append(matchedEntry);
|
|
129
|
+
const found = await search({ query: token, limit: 5 });
|
|
130
|
+
const hit = found.items.find((item) => item.sessionId === searchSessionId);
|
|
131
|
+
if (!hit) {
|
|
132
|
+
throw new Error("searchSessions must find a session by matching message text");
|
|
133
|
+
}
|
|
134
|
+
if (hit.entryId !== matchedEntry.id) {
|
|
135
|
+
throw new Error(`searchSessions must point at the matched entry; got ${String(hit.entryId)}`);
|
|
136
|
+
}
|
|
137
|
+
if (hit.runId !== matchedEntry.runId) {
|
|
138
|
+
throw new Error("searchSessions must carry the matched entry runId");
|
|
139
|
+
}
|
|
140
|
+
if (typeof hit.snippet !== "string" || !hit.snippet.includes(token)) {
|
|
141
|
+
throw new Error("searchSessions snippet must contain the matched text");
|
|
142
|
+
}
|
|
143
|
+
if (!Number.isSafeInteger(hit.turn) || hit.turn < 1) {
|
|
144
|
+
throw new Error("searchSessions must carry a 1-based matched-entry turn index");
|
|
145
|
+
}
|
|
146
|
+
const annotationOnly = await search({ query: token, kind: "summary", limit: 5 });
|
|
147
|
+
if (annotationOnly.items.some((item) => item.sessionId === searchSessionId)) {
|
|
148
|
+
throw new Error("searchSessions kind filter must exclude non-matching entry kinds");
|
|
149
|
+
}
|
|
150
|
+
// One hit per session: a second matching entry must not add a second row for the same session.
|
|
151
|
+
await store.append({
|
|
152
|
+
id: "conformance-search-entry-2",
|
|
153
|
+
parentId: matchedEntry.id,
|
|
154
|
+
sessionId: searchSessionId,
|
|
155
|
+
timestamp: "2026-01-01T00:00:06.000Z",
|
|
156
|
+
kind: "summary",
|
|
157
|
+
summary: `${token} recap`,
|
|
158
|
+
});
|
|
159
|
+
const deduped = await search({ query: token, limit: 5 });
|
|
160
|
+
const sessionHits = deduped.items.filter((item) => item.sessionId === searchSessionId);
|
|
161
|
+
if (sessionHits.length !== 1) {
|
|
162
|
+
throw new Error(`searchSessions must return one hit per session; got ${sessionHits.length}`);
|
|
163
|
+
}
|
|
116
164
|
const empty = await search(resolveSessionSearchQuery({
|
|
117
165
|
workspaceRoot: "__prism_conformance_empty__",
|
|
118
166
|
limit: 5,
|
package/dist/tools.d.ts
CHANGED
|
@@ -58,6 +58,11 @@ export declare function createToolRegistry(tools?: readonly ToolDefinition[], op
|
|
|
58
58
|
export declare function filterTools(tools: readonly ToolDefinition[], filter?: ToolFilterInput): readonly ToolDefinition[];
|
|
59
59
|
/** Cap matches tool-search index; run allow-lists never exceed the disclosed set. */
|
|
60
60
|
export declare const HARD_RUN_TOOL_NAMES = 1024;
|
|
61
|
+
/** Restrictive clamp: keep listed order; names outside the grant are dropped (not thrown). */
|
|
62
|
+
export declare function clampTurnToolNames(listed: readonly ToolDefinition[], requested: readonly string[]): {
|
|
63
|
+
readonly tools: readonly ToolDefinition[];
|
|
64
|
+
readonly dropped: readonly string[];
|
|
65
|
+
};
|
|
61
66
|
/**
|
|
62
67
|
* Per-run allow-list. Omitted grant → unchanged list. Checkpointed grant cannot widen.
|
|
63
68
|
* Fresh unknown names fail closed; resume drops names the current registry no longer has.
|
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isJsonObject } from "./config.js";
|
|
2
|
-
import { GuardrailError, runGuardrails } from "./guardrails.js";
|
|
2
|
+
import { GuardrailError, guardrailRefusalText, runGuardrails } from "./guardrails.js";
|
|
3
3
|
import { assertIdentityActive, assertIdentityMatchesOwnership, ownershipFromIdentity } from "./identity.js";
|
|
4
4
|
import { createId } from "./ids.js";
|
|
5
5
|
import { errorToErrorInfo, redactRunLedgerRecord, redactSecrets } from "./redaction.js";
|
|
@@ -61,15 +61,15 @@ export function filterTools(tools, filter) {
|
|
|
61
61
|
/** Cap matches tool-search index; run allow-lists never exceed the disclosed set. */
|
|
62
62
|
export const HARD_RUN_TOOL_NAMES = 1024;
|
|
63
63
|
const MAX_RUN_TOOL_NAME_CHARS = 256;
|
|
64
|
-
function assertRunToolNames(names) {
|
|
64
|
+
function assertRunToolNames(names, label = "RunOptions.toolNames") {
|
|
65
65
|
if (names.length > HARD_RUN_TOOL_NAMES) {
|
|
66
|
-
throw new TypeError(
|
|
66
|
+
throw new TypeError(`${label} exceeds ${HARD_RUN_TOOL_NAMES} entries`);
|
|
67
67
|
}
|
|
68
68
|
const out = [];
|
|
69
69
|
const seen = new Set();
|
|
70
70
|
for (const name of names) {
|
|
71
71
|
if (typeof name !== "string" || name.length === 0 || name.length > MAX_RUN_TOOL_NAME_CHARS) {
|
|
72
|
-
throw new TypeError(
|
|
72
|
+
throw new TypeError(`${label} entries must be non-empty strings of at most ${MAX_RUN_TOOL_NAME_CHARS} characters`);
|
|
73
73
|
}
|
|
74
74
|
if (!seen.has(name)) {
|
|
75
75
|
seen.add(name);
|
|
@@ -78,6 +78,14 @@ function assertRunToolNames(names) {
|
|
|
78
78
|
}
|
|
79
79
|
return out;
|
|
80
80
|
}
|
|
81
|
+
/** Restrictive clamp: keep listed order; names outside the grant are dropped (not thrown). */
|
|
82
|
+
export function clampTurnToolNames(listed, requested) {
|
|
83
|
+
const names = assertRunToolNames(requested, "toolNarrowing");
|
|
84
|
+
const grant = new Set(listed.map((tool) => tool.name));
|
|
85
|
+
const dropped = names.filter((name) => !grant.has(name));
|
|
86
|
+
const allow = names.filter((name) => grant.has(name));
|
|
87
|
+
return { tools: allow.length === 0 ? [] : filterTools(listed, { allow }), dropped };
|
|
88
|
+
}
|
|
81
89
|
/**
|
|
82
90
|
* Per-run allow-list. Omitted grant → unchanged list. Checkpointed grant cannot widen.
|
|
83
91
|
* Fresh unknown names fail closed; resume drops names the current registry no longer has.
|
|
@@ -125,7 +133,7 @@ export async function dispatchToolCall(options) {
|
|
|
125
133
|
if (inputGuards.terminal) {
|
|
126
134
|
if (inputGuards.terminal.action !== "block")
|
|
127
135
|
throw new GuardrailError(inputGuards.terminal);
|
|
128
|
-
return blocked(mediatedCall, options.context, "guardrail_blocked", { message:
|
|
136
|
+
return blocked(mediatedCall, options.context, "guardrail_blocked", { message: guardrailBlockMessage(inputGuards.terminal) }, options, startedAt);
|
|
129
137
|
}
|
|
130
138
|
const tool = options.registry.get(mediatedCall.name);
|
|
131
139
|
const postcheck = await checkCall(mediatedCall, options, startedAt);
|
|
@@ -232,7 +240,7 @@ export async function dispatchToolCall(options) {
|
|
|
232
240
|
throw new GuardrailError(outputGuards.terminal);
|
|
233
241
|
if (effect)
|
|
234
242
|
return finishUnknownEffect(effect, mediatedCall, context, options, startedAt);
|
|
235
|
-
return blocked(mediatedCall, context, "guardrail_blocked", { message:
|
|
243
|
+
return blocked(mediatedCall, context, "guardrail_blocked", { message: guardrailBlockMessage(outputGuards.terminal) }, options, startedAt);
|
|
236
244
|
}
|
|
237
245
|
if (effect && mediatedResult.error)
|
|
238
246
|
return finishUnknownEffect(effect, mediatedCall, context, options, startedAt);
|
|
@@ -442,6 +450,13 @@ function isSuspended(error) {
|
|
|
442
450
|
function isLoopStateError(error) {
|
|
443
451
|
return typeof error?.code === "string" && error.code.startsWith("ERR_PRISM_LOOP_");
|
|
444
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Plan 104 T3/T4: the model-visible refusal line for a terminal guardrail decision. `guardrailRefusalText`
|
|
455
|
+
* names a compiled pack rule (bounded, redacted); any other guardrail keeps the neutral stage text.
|
|
456
|
+
*/
|
|
457
|
+
function guardrailBlockMessage(record) {
|
|
458
|
+
return (guardrailRefusalText(record) ?? (record.stage === "tool_output" ? "Tool result blocked by guardrail" : "Tool call blocked by guardrail"));
|
|
459
|
+
}
|
|
445
460
|
function isDelegationSuspended(error) {
|
|
446
461
|
return error?.code === "ERR_PRISM_DELEGATION_SUSPENDED";
|
|
447
462
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Model-family chars/token tables and the family text estimator (plan 091 Task 1).
|
|
2
|
+
*
|
|
3
|
+
* Pure and O(text length): no network, no I/O, no content retention. These are
|
|
4
|
+
* heuristics, not tokenizers — harnesses universally approximate. Reported usage
|
|
5
|
+
* always wins; estimates exist so missing usage is never shown as zero.
|
|
6
|
+
*
|
|
7
|
+
* Ratio provenance: `openai` is calibrated against `o200k_base` counts on the
|
|
8
|
+
* in-repo fixtures (`src/__tests__/usage-estimation.test.ts`); the other families
|
|
9
|
+
* use their published tokenizer guidance ranges and are intentionally rounded
|
|
10
|
+
* toward over-counting, because an overestimated context meter is safe while an
|
|
11
|
+
* underestimated one under-compacts. `unknown` is the most conservative table so
|
|
12
|
+
* an unidentified model can never look smaller than a known one.
|
|
13
|
+
*/
|
|
14
|
+
import type { ModelFamily, TokenEstimateConfidence } from "./contracts-core/usage.js";
|
|
15
|
+
/** Row of {@link MODEL_FAMILY_TOKENS}: prose chars per token, chat-template
|
|
16
|
+
* tokens added once per message, and the confidence label for the table. */
|
|
17
|
+
export interface ModelFamilyTokens {
|
|
18
|
+
readonly charsPerToken: number;
|
|
19
|
+
readonly perMessageOverhead: number;
|
|
20
|
+
readonly confidence: TokenEstimateConfidence;
|
|
21
|
+
}
|
|
22
|
+
/** Per-family chars/token tables (plan 091 Task 1). Estimates only, never billing. */
|
|
23
|
+
export declare const MODEL_FAMILY_TOKENS: Readonly<Record<ModelFamily, ModelFamilyTokens>>;
|
|
24
|
+
/** Resolve a model id (e.g. `"claude-sonnet-4.5"`), provider id, or family name
|
|
25
|
+
* to a table key. Unmatched input is `"unknown"` — never a throw. */
|
|
26
|
+
export declare function resolveModelFamily(model?: string): ModelFamily;
|
|
27
|
+
/** Estimate tokens for one flattened text under a family's ratios. The message
|
|
28
|
+
* level (`estimateMessageTokens`) owns per-message overhead; this is text-only. */
|
|
29
|
+
export declare function estimateTextTokensForFamily(text: string, modelFamily?: string): number;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** CJK ideographs/kana/hangul tokenize near 1.5 chars/token in modern family tokenizers. */
|
|
2
|
+
const CJK_CHARS_PER_TOKEN = 1.5;
|
|
3
|
+
/** Fenced code tokenizes worse than prose: code ratio = prose ratio * this factor. */
|
|
4
|
+
const CODE_RATIO_FACTOR = 0.88;
|
|
5
|
+
/** Per-family chars/token tables (plan 091 Task 1). Estimates only, never billing. */
|
|
6
|
+
export const MODEL_FAMILY_TOKENS = {
|
|
7
|
+
anthropic: { charsPerToken: 3.7, perMessageOverhead: 4, confidence: "medium" },
|
|
8
|
+
openai: { charsPerToken: 5.0, perMessageOverhead: 3, confidence: "medium" },
|
|
9
|
+
google: { charsPerToken: 3.9, perMessageOverhead: 4, confidence: "medium" },
|
|
10
|
+
deepseek: { charsPerToken: 3.8, perMessageOverhead: 4, confidence: "medium" },
|
|
11
|
+
"openrouter-generic": { charsPerToken: 4.4, perMessageOverhead: 4, confidence: "medium" },
|
|
12
|
+
mistral: { charsPerToken: 3.9, perMessageOverhead: 3, confidence: "medium" },
|
|
13
|
+
unknown: { charsPerToken: 3.5, perMessageOverhead: 6, confidence: "low" },
|
|
14
|
+
};
|
|
15
|
+
/** Model-id patterns per family. Family names themselves also resolve (see `resolveModelFamily`). */
|
|
16
|
+
const FAMILY_PATTERNS = [
|
|
17
|
+
["anthropic", /claude|anthropic/i],
|
|
18
|
+
["openai", /gpt-|openai|chatgpt|codex|^o[1-9]/i],
|
|
19
|
+
["google", /gemini|gemma|palm|google/i],
|
|
20
|
+
["deepseek", /deepseek/i],
|
|
21
|
+
["mistral", /mistral|mixtral|codestral|magistral|devstral|pixtral|ministral/i],
|
|
22
|
+
["openrouter-generic", /openrouter/i],
|
|
23
|
+
];
|
|
24
|
+
/** Resolve a model id (e.g. `"claude-sonnet-4.5"`), provider id, or family name
|
|
25
|
+
* to a table key. Unmatched input is `"unknown"` — never a throw. */
|
|
26
|
+
export function resolveModelFamily(model) {
|
|
27
|
+
if (typeof model !== "string")
|
|
28
|
+
return "unknown";
|
|
29
|
+
const id = model.trim();
|
|
30
|
+
if (id in MODEL_FAMILY_TOKENS)
|
|
31
|
+
return id;
|
|
32
|
+
for (const [family, pattern] of FAMILY_PATTERNS) {
|
|
33
|
+
if (pattern.test(id))
|
|
34
|
+
return family;
|
|
35
|
+
}
|
|
36
|
+
return "unknown";
|
|
37
|
+
}
|
|
38
|
+
/** Estimate tokens for one flattened text under a family's ratios. The message
|
|
39
|
+
* level (`estimateMessageTokens`) owns per-message overhead; this is text-only. */
|
|
40
|
+
export function estimateTextTokensForFamily(text, modelFamily) {
|
|
41
|
+
const { charsPerToken } = MODEL_FAMILY_TOKENS[resolveModelFamily(modelFamily)];
|
|
42
|
+
let cjk = 0;
|
|
43
|
+
for (let index = 0; index < text.length;) {
|
|
44
|
+
const codePoint = text.codePointAt(index) ?? 0; // unreachable 0: index < length; avoids a non-null assertion
|
|
45
|
+
if (isCjkCodePoint(codePoint))
|
|
46
|
+
cjk += 1;
|
|
47
|
+
index += codePoint > 0xffff ? 2 : 1;
|
|
48
|
+
}
|
|
49
|
+
const code = fencedChars(text);
|
|
50
|
+
const other = Math.max(0, text.length - cjk - code);
|
|
51
|
+
return Math.ceil(cjk / CJK_CHARS_PER_TOKEN + code / (charsPerToken * CODE_RATIO_FACTOR) + other / charsPerToken);
|
|
52
|
+
}
|
|
53
|
+
/** Length of the characters enclosed by ``` fence pairs (unclosed fence runs to the end). */
|
|
54
|
+
function fencedChars(text) {
|
|
55
|
+
let total = 0;
|
|
56
|
+
let index = text.indexOf("```");
|
|
57
|
+
while (index !== -1) {
|
|
58
|
+
const end = text.indexOf("```", index + 3);
|
|
59
|
+
if (end === -1) {
|
|
60
|
+
total += text.length - index;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
total += end + 3 - index;
|
|
64
|
+
index = text.indexOf("```", end + 3);
|
|
65
|
+
}
|
|
66
|
+
return total;
|
|
67
|
+
}
|
|
68
|
+
/** Han, kana, hangul, CJK punctuation/fullwidth, and ext-B+ ideograph ranges. */
|
|
69
|
+
function isCjkCodePoint(codePoint) {
|
|
70
|
+
return ((codePoint >= 0x3000 && codePoint <= 0x303f) ||
|
|
71
|
+
(codePoint >= 0x3040 && codePoint <= 0x30ff) ||
|
|
72
|
+
(codePoint >= 0x3400 && codePoint <= 0x4dbf) ||
|
|
73
|
+
(codePoint >= 0x4e00 && codePoint <= 0x9fff) ||
|
|
74
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7af) ||
|
|
75
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
76
|
+
(codePoint >= 0xff00 && codePoint <= 0xffef) ||
|
|
77
|
+
(codePoint >= 0x20000 && codePoint <= 0x2fa1f));
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=usage-estimation.js.map
|