@giovannijecha/jecode 0.8.6 → 0.8.7
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/README.md +18 -8
- package/assets/tokenizers/LICENSE +21 -0
- package/assets/tokenizers/o200k-base.tiktoken.gz +0 -0
- package/dist/cli-info.js +8 -13
- package/dist/commands.js +7 -3
- package/dist/config.js +33 -47
- package/dist/context/automatic.js +16 -1
- package/dist/context/budget.js +5 -2
- package/dist/context/compactor.js +91 -37
- package/dist/context/diagnostics.js +108 -0
- package/dist/context/lifetime.js +18 -0
- package/dist/context/manager.js +67 -0
- package/dist/context/manual.js +11 -10
- package/dist/context/measurement.js +75 -0
- package/dist/context/policy.js +13 -10
- package/dist/context/request-observation.js +46 -0
- package/dist/context/request-projection.js +8 -34
- package/dist/context/request.js +22 -0
- package/dist/context/tokenizer/bpe.js +68 -0
- package/dist/context/tokenizer/o200k.js +54 -0
- package/dist/context/tokenizer/vocabulary.js +34 -0
- package/dist/controller-request.js +47 -45
- package/dist/controller.js +13 -2
- package/dist/input-boundary.js +0 -62
- package/dist/launch.js +13 -9
- package/dist/model-command.js +7 -17
- package/dist/oauth-result-page.js +68 -0
- package/dist/openai-oauth-callback.js +2 -50
- package/dist/permission-command.js +9 -24
- package/dist/permissions.js +10 -8
- package/dist/providers/anthropic-wire.js +1 -1
- package/dist/providers/anthropic.js +3 -1
- package/dist/providers/input-measurement.js +85 -0
- package/dist/providers/ollama-wire.js +1 -1
- package/dist/providers/ollama.js +2 -0
- package/dist/providers/openai-codex.js +3 -0
- package/dist/providers/openai-stream.js +20 -8
- package/dist/providers/openai-summary.js +37 -0
- package/dist/providers/openai-wire.js +1 -1
- package/dist/providers/openai.js +3 -0
- package/dist/settings-command.js +5 -10
- package/dist/settings.js +1 -1
- package/dist/start.js +35 -51
- package/dist/timeline.js +2 -0
- package/dist/tui/activity.js +1 -1
- package/dist/tui/app-input.js +34 -17
- package/dist/tui/app-workflows.js +7 -2
- package/dist/tui/app.js +55 -28
- package/dist/tui/approve.js +3 -4
- package/dist/tui/blocks.js +5 -7
- package/dist/tui/command-workflow.js +14 -8
- package/dist/tui/components/command-menu.js +1 -1
- package/dist/tui/components/menu.js +19 -11
- package/dist/tui/components/status.js +1 -1
- package/dist/tui/frame.js +8 -3
- package/dist/tui/help.js +2 -1
- package/dist/tui/picker-layout.js +5 -1
- package/dist/tui/screen.js +7 -0
- package/dist/tui/session-view.js +0 -1
- package/dist/tui/transcript-grammar.js +1 -8
- package/dist/tui/transcript-view.js +4 -0
- package/dist/tui/turn-workflow.js +47 -76
- package/dist/tui/view.js +4 -4
- package/dist/ui/render.js +0 -4
- package/package.json +5 -1
- package/dist/batch-view.js +0 -42
- package/dist/batch.js +0 -269
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// One automatic-compaction lifecycle, independent of terminal and persistence.
|
|
2
|
+
import { automaticCompactionKey } from "./automatic.js";
|
|
3
|
+
import { compactContext } from "./compactor.js";
|
|
4
|
+
import { isContextOverflow } from "./policy.js";
|
|
5
|
+
export function contextManager(options) {
|
|
6
|
+
let anchor;
|
|
7
|
+
return {
|
|
8
|
+
get anchor() {
|
|
9
|
+
return anchor;
|
|
10
|
+
},
|
|
11
|
+
async compact(canonical, projected, request) {
|
|
12
|
+
const force = request.reason === "overflow";
|
|
13
|
+
if (force && (request.error === undefined || !isContextOverflow(request.error)))
|
|
14
|
+
return undefined;
|
|
15
|
+
if (!force && request.inputTokens < request.policy.triggerTokens)
|
|
16
|
+
return undefined;
|
|
17
|
+
const nodeId = options.nodeId();
|
|
18
|
+
const attempt = {
|
|
19
|
+
key: automaticCompactionKey(options.provider.id, options.model, nodeId, canonical.length),
|
|
20
|
+
scope: `${options.provider.id}\0${options.model}\0${options.identity.conversationId}\0` +
|
|
21
|
+
`${request.policy.windowTokens}\0${request.policy.triggerTokens}`,
|
|
22
|
+
reason: request.reason,
|
|
23
|
+
inputTokens: request.inputTokens,
|
|
24
|
+
retryGrowthTokens: Math.max(1_024, Math.min(request.policy.minimumPrefixTokens, request.policy.requestLimitTokens - 256 - request.inputTokens)),
|
|
25
|
+
};
|
|
26
|
+
if (!options.gate.allows(attempt))
|
|
27
|
+
return undefined;
|
|
28
|
+
let started = false;
|
|
29
|
+
const result = await compactContext({
|
|
30
|
+
reason: request.reason,
|
|
31
|
+
onDiagnostic: options.onDiagnostic,
|
|
32
|
+
provider: options.provider,
|
|
33
|
+
model: options.model,
|
|
34
|
+
effort: options.effort,
|
|
35
|
+
context: projected,
|
|
36
|
+
turn: canonical.slice(options.historyStart),
|
|
37
|
+
nodeId,
|
|
38
|
+
coveredMessages: anchor?.messageCount ?? 0,
|
|
39
|
+
lastInputTokens: 0,
|
|
40
|
+
estimatedInputTokens: request.inputTokens,
|
|
41
|
+
force,
|
|
42
|
+
policy: request.policy,
|
|
43
|
+
signal: options.signal,
|
|
44
|
+
requestEnvelope: {
|
|
45
|
+
system: options.system,
|
|
46
|
+
tools: options.tools,
|
|
47
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
48
|
+
},
|
|
49
|
+
requestIdentity: options.identity,
|
|
50
|
+
onBegin() {
|
|
51
|
+
started = true;
|
|
52
|
+
options.onStatus(true);
|
|
53
|
+
},
|
|
54
|
+
onEnd() { options.onStatus(false); },
|
|
55
|
+
onUsage: options.onUsage,
|
|
56
|
+
});
|
|
57
|
+
if (result === undefined) {
|
|
58
|
+
if (started && options.signal?.aborted !== true)
|
|
59
|
+
options.gate.failed(attempt);
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
options.gate.succeeded(attempt);
|
|
63
|
+
anchor = result.anchor;
|
|
64
|
+
return result.messages;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/context/manual.js
CHANGED
|
@@ -7,9 +7,8 @@ import { recordAuxiliaryUsage } from "../usage.js";
|
|
|
7
7
|
import { toolSpecs } from "../tools/index.js";
|
|
8
8
|
import { resolveContextPolicy } from "./capacity.js";
|
|
9
9
|
import { compactContext } from "./compactor.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { projectToolResultsNewest, toolResultProjectionBudget } from "./request-projection.js";
|
|
10
|
+
import { measureInput, messageCounter } from "./measurement.js";
|
|
11
|
+
import { planCompaction } from "./policy.js";
|
|
13
12
|
import { requestIdentityForSession } from "../request-identity.js";
|
|
14
13
|
const MIN_PREFIX_TOKENS = 512;
|
|
15
14
|
export async function compactSession(session, options = {}) {
|
|
@@ -29,9 +28,11 @@ export async function compactSession(session, options = {}) {
|
|
|
29
28
|
onStatus: (status) => options.onStatus?.(status),
|
|
30
29
|
});
|
|
31
30
|
const specs = toolSpecs(session.tools);
|
|
32
|
-
const estimatedInputTokens = await
|
|
31
|
+
const estimatedInputTokens = await measureInput(session.provider, {
|
|
32
|
+
model: session.model,
|
|
33
|
+
effort: session.config.effort,
|
|
33
34
|
system: session.system,
|
|
34
|
-
messages:
|
|
35
|
+
messages: context,
|
|
35
36
|
tools: specs,
|
|
36
37
|
}, options.signal);
|
|
37
38
|
if (estimatedInputTokens < MIN_PREFIX_TOKENS) {
|
|
@@ -41,9 +42,10 @@ export async function compactSession(session, options = {}) {
|
|
|
41
42
|
const coveredMessages = active.context?.throughNodeId === active.id
|
|
42
43
|
? active.context.messageCount
|
|
43
44
|
: 0;
|
|
44
|
-
const
|
|
45
|
+
const countMessages = await messageCounter(session.provider, session.model, session.config.effort, options.signal);
|
|
46
|
+
const plan = await planCompaction(context, active.messages, coveredMessages, 0, true, policy, estimatedInputTokens, options.signal, countMessages);
|
|
45
47
|
if (plan === undefined ||
|
|
46
|
-
await
|
|
48
|
+
await countMessages(plan.prefix, options.signal) < MIN_PREFIX_TOKENS) {
|
|
47
49
|
options.onStatus?.();
|
|
48
50
|
return "unchanged";
|
|
49
51
|
}
|
|
@@ -55,7 +57,7 @@ export async function compactSession(session, options = {}) {
|
|
|
55
57
|
turn: active.messages,
|
|
56
58
|
nodeId: active.id,
|
|
57
59
|
coveredMessages,
|
|
58
|
-
lastInputTokens:
|
|
60
|
+
lastInputTokens: 0,
|
|
59
61
|
estimatedInputTokens,
|
|
60
62
|
precomputedPlan: plan,
|
|
61
63
|
signal: options.signal,
|
|
@@ -70,11 +72,10 @@ export async function compactSession(session, options = {}) {
|
|
|
70
72
|
requestIdentity: requestIdentityForSession(session),
|
|
71
73
|
onBegin: () => options.onStatus?.("Compacting"),
|
|
72
74
|
onEnd: () => options.onStatus?.(),
|
|
75
|
+
onUsage: (usage) => recordAuxiliaryUsage(session.usage, usage),
|
|
73
76
|
});
|
|
74
77
|
if (result === undefined)
|
|
75
78
|
throw new Error("context could not be compacted");
|
|
76
|
-
if (result.usage !== undefined)
|
|
77
|
-
recordAuxiliaryUsage(session.usage, result.usage);
|
|
78
79
|
const next = session.conversation.commit({
|
|
79
80
|
nodeId: active.id,
|
|
80
81
|
parentId: active.parentId,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Local input estimates anchored only to an unchanged, provider-measured prefix.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { estimateRequestInputTokensResponsive } from "./budget.js";
|
|
4
|
+
/** A response's usage is evidence about that exact request, never later history. */
|
|
5
|
+
export function inputMeter(provider) {
|
|
6
|
+
let baseline;
|
|
7
|
+
let epoch = 0;
|
|
8
|
+
return {
|
|
9
|
+
async measure(input, signal) {
|
|
10
|
+
const measuredEpoch = epoch;
|
|
11
|
+
const estimatedTokens = await measureInput(provider, input, signal);
|
|
12
|
+
const envelope = fingerprint({
|
|
13
|
+
provider: provider.id,
|
|
14
|
+
model: input.model,
|
|
15
|
+
effort: input.effort,
|
|
16
|
+
system: input.system,
|
|
17
|
+
tools: input.tools,
|
|
18
|
+
});
|
|
19
|
+
const messages = [];
|
|
20
|
+
for (const message of input.messages) {
|
|
21
|
+
signal?.throwIfAborted();
|
|
22
|
+
// Including raw and usage is deliberately conservative: even a change
|
|
23
|
+
// to an opaque reserve must invalidate the old provider observation.
|
|
24
|
+
messages.push(fingerprint(message));
|
|
25
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
26
|
+
}
|
|
27
|
+
const previous = baseline?.measurement;
|
|
28
|
+
const anchored = measuredEpoch === epoch && previous !== undefined && previous.envelope === envelope &&
|
|
29
|
+
previous.messages.length <= messages.length &&
|
|
30
|
+
previous.messages.every((value, index) => value === messages[index]);
|
|
31
|
+
const inputTokens = anchored && baseline !== undefined
|
|
32
|
+
? baseline.reported + Math.max(0, estimatedTokens - baseline.measurement.estimatedTokens)
|
|
33
|
+
: estimatedTokens;
|
|
34
|
+
if (!anchored && measuredEpoch === epoch)
|
|
35
|
+
baseline = undefined;
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
epoch: measuredEpoch,
|
|
38
|
+
inputTokens,
|
|
39
|
+
estimatedTokens,
|
|
40
|
+
source: anchored ? "provider-prefix" : "estimate",
|
|
41
|
+
envelope,
|
|
42
|
+
messages: Object.freeze(messages),
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
observe(measurement, reported) {
|
|
46
|
+
if (measurement.epoch !== epoch || reported === undefined || !Number.isSafeInteger(reported) || reported <= 0)
|
|
47
|
+
return;
|
|
48
|
+
baseline = { measurement, reported };
|
|
49
|
+
},
|
|
50
|
+
reset() {
|
|
51
|
+
epoch++;
|
|
52
|
+
baseline = undefined;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function measureInput(provider, input, signal) {
|
|
57
|
+
signal?.throwIfAborted();
|
|
58
|
+
const tokens = provider.measureInput === undefined
|
|
59
|
+
? await estimateRequestInputTokensResponsive(input, signal)
|
|
60
|
+
: await provider.measureInput(input, signal);
|
|
61
|
+
signal?.throwIfAborted();
|
|
62
|
+
if (!Number.isSafeInteger(tokens) || tokens <= 0) {
|
|
63
|
+
throw new Error("provider returned an invalid input token estimate");
|
|
64
|
+
}
|
|
65
|
+
return tokens;
|
|
66
|
+
}
|
|
67
|
+
/** Retention must include opaque replay reserves, excluding the fixed envelope. */
|
|
68
|
+
export async function messageCounter(provider, model, effort, signal) {
|
|
69
|
+
const input = { model, effort, system: "", messages: [], tools: [] };
|
|
70
|
+
const overhead = await measureInput(provider, input, signal);
|
|
71
|
+
return async (messages, currentSignal = signal) => Math.max(1, await measureInput(provider, { ...input, messages: [...messages] }, currentSignal) - overhead);
|
|
72
|
+
}
|
|
73
|
+
function fingerprint(value) {
|
|
74
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
75
|
+
}
|
package/dist/context/policy.js
CHANGED
|
@@ -6,13 +6,13 @@ export const MAX_COMPACTION_PERCENT = 95;
|
|
|
6
6
|
export const FALLBACK_CONTEXT_WINDOW_TOKENS = 200_000;
|
|
7
7
|
export const REQUEST_ESTIMATE_HEADROOM_PERCENT = 5;
|
|
8
8
|
export const MIN_REQUEST_OUTPUT_TOKENS = 256;
|
|
9
|
-
export async function planCompaction(context, turn, coveredMessages, lastInputTokens, force, policy, estimatedInputTokens, signal) {
|
|
9
|
+
export async function planCompaction(context, turn, coveredMessages, lastInputTokens, force, policy, estimatedInputTokens, signal, countMessages = estimateTokensResponsive) {
|
|
10
10
|
if (!validPolicy(policy) || coveredMessages < 0 || coveredMessages > turn.length)
|
|
11
11
|
return undefined;
|
|
12
12
|
if (estimatedInputTokens !== undefined &&
|
|
13
13
|
(!Number.isSafeInteger(estimatedInputTokens) || estimatedInputTokens <= 0))
|
|
14
14
|
return undefined;
|
|
15
|
-
const estimated = estimatedInputTokens ?? await
|
|
15
|
+
const estimated = estimatedInputTokens ?? await countMessages(context, signal);
|
|
16
16
|
if (!force &&
|
|
17
17
|
(estimated < policy.targetTokens || Math.max(estimated, lastInputTokens) < policy.triggerTokens))
|
|
18
18
|
return undefined;
|
|
@@ -23,13 +23,13 @@ export async function planCompaction(context, turn, coveredMessages, lastInputTo
|
|
|
23
23
|
if (!await sameMessages(context.slice(contextPrefix), turn.slice(coveredMessages), signal))
|
|
24
24
|
return undefined;
|
|
25
25
|
const targetTokens = force
|
|
26
|
-
? Math.min(policy.targetTokens, Math.max(512, Math.floor(estimated /
|
|
26
|
+
? Math.min(policy.targetTokens, Math.max(512, Math.floor(estimated / 2)))
|
|
27
27
|
: policy.targetTokens;
|
|
28
28
|
const recentTokens = Math.min(policy.recentTokens, Math.max(256, Math.floor(targetTokens / 2)));
|
|
29
|
-
const recent = await recentBoundary(turn, coveredMessages, recentTokens, signal);
|
|
29
|
+
const recent = await recentBoundary(turn, coveredMessages, recentTokens, signal, countMessages);
|
|
30
30
|
let boundary = recent.boundary;
|
|
31
31
|
let tail = turn.slice(boundary);
|
|
32
|
-
if (turn.length > 1 && recent.tokens >
|
|
32
|
+
if (turn.length > 1 && recent.tokens > policy.requestLimitTokens) {
|
|
33
33
|
boundary = turn.length;
|
|
34
34
|
tail = [];
|
|
35
35
|
}
|
|
@@ -37,7 +37,7 @@ export async function planCompaction(context, turn, coveredMessages, lastInputTo
|
|
|
37
37
|
if (prefixEnd <= 0 || prefixEnd > context.length)
|
|
38
38
|
return undefined;
|
|
39
39
|
const prefix = context.slice(0, prefixEnd);
|
|
40
|
-
if (!force && await
|
|
40
|
+
if (!force && await countMessages(prefix, signal) < policy.minimumPrefixTokens) {
|
|
41
41
|
return undefined;
|
|
42
42
|
}
|
|
43
43
|
return {
|
|
@@ -73,10 +73,13 @@ export function policyForContextWindow(context, compactionPercent) {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
export function estimateTokens(messages) {
|
|
76
|
-
return estimateSerializedTokens(messages) + messages.length * 8;
|
|
76
|
+
return estimateSerializedTokens(normalized(messages)) + messages.length * 8;
|
|
77
77
|
}
|
|
78
78
|
export async function estimateTokensResponsive(messages, signal) {
|
|
79
|
-
return await estimateSerializedTokensResponsive(messages, signal) + messages.length * 8;
|
|
79
|
+
return await estimateSerializedTokensResponsive(normalized(messages), signal) + messages.length * 8;
|
|
80
|
+
}
|
|
81
|
+
function normalized(messages) {
|
|
82
|
+
return messages.map(({ role, content }) => ({ role, content }));
|
|
80
83
|
}
|
|
81
84
|
export function isContextOverflow(error) {
|
|
82
85
|
const candidate = error;
|
|
@@ -85,7 +88,7 @@ export function isContextOverflow(error) {
|
|
|
85
88
|
const detail = `${candidate.message}\n${candidate.body ?? ""}`;
|
|
86
89
|
return /context_length_exceeded|maximum context length|context window|prompt is too long|input (?:is )?too (?:long|large)|(?:input|prompt|context).{0,80}(?:exceed|maximum|max tokens)/i.test(detail);
|
|
87
90
|
}
|
|
88
|
-
async function recentBoundary(turn, coveredMessages, recentTokens, signal) {
|
|
91
|
+
async function recentBoundary(turn, coveredMessages, recentTokens, signal, countMessages) {
|
|
89
92
|
const minimum = Math.max(coveredMessages, minimumRecentBoundary(turn));
|
|
90
93
|
const candidates = [];
|
|
91
94
|
for (let candidate = coveredMessages; candidate <= minimum; candidate++) {
|
|
@@ -96,7 +99,7 @@ async function recentBoundary(turn, coveredMessages, recentTokens, signal) {
|
|
|
96
99
|
const estimateAt = (candidate) => {
|
|
97
100
|
let estimate = cache.get(candidate);
|
|
98
101
|
if (estimate === undefined) {
|
|
99
|
-
estimate =
|
|
102
|
+
estimate = countMessages(turn.slice(candidate), signal);
|
|
100
103
|
cache.set(candidate, estimate);
|
|
101
104
|
}
|
|
102
105
|
return estimate;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { publishDiagnostic } from "./diagnostics.js";
|
|
2
|
+
export async function observePreparation(policy, reason, signal, prepare) {
|
|
3
|
+
const started = performance.now();
|
|
4
|
+
try {
|
|
5
|
+
return await prepare();
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
publishDiagnostic({ kind: "preparation", reason, outcome: signal?.aborted ? "cancelled" : "failed",
|
|
9
|
+
elapsedMs: Math.round(performance.now() - started), windowTokens: policy.windowTokens,
|
|
10
|
+
triggerTokens: policy.triggerTokens, requestLimitTokens: policy.requestLimitTokens });
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function sendObserved(provider, request, measurement, policy, preparationMs, clippedResults) {
|
|
15
|
+
const started = performance.now();
|
|
16
|
+
let firstEventMs;
|
|
17
|
+
let response;
|
|
18
|
+
let outcome = "failed";
|
|
19
|
+
try {
|
|
20
|
+
response = await provider.send({ ...request, onStream(event) {
|
|
21
|
+
firstEventMs ??= Math.round(performance.now() - started);
|
|
22
|
+
request.onStream?.(event);
|
|
23
|
+
} });
|
|
24
|
+
request.signal?.throwIfAborted();
|
|
25
|
+
outcome = "completed";
|
|
26
|
+
return response;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (request.signal?.aborted)
|
|
30
|
+
outcome = "cancelled";
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
publishDiagnostic({
|
|
35
|
+
kind: "request", source: measurement.source, outcome,
|
|
36
|
+
tokenization: provider.inputTokenization?.(request.model) ?? "heuristic",
|
|
37
|
+
estimatedTokens: measurement.estimatedTokens, inputTokens: measurement.inputTokens,
|
|
38
|
+
windowTokens: policy.windowTokens, triggerTokens: policy.triggerTokens,
|
|
39
|
+
requestLimitTokens: policy.requestLimitTokens, outputBudgetTokens: request.maxTokens,
|
|
40
|
+
preparationMs: Math.round(preparationMs), providerMs: Math.round(performance.now() - started),
|
|
41
|
+
clippedResults,
|
|
42
|
+
...(firstEventMs === undefined ? {} : { firstEventMs }),
|
|
43
|
+
...(response?.usage === undefined ? {} : { reportedInputTokens: response.usage.inputTokens }),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -3,43 +3,17 @@
|
|
|
3
3
|
import { leadingText, trailingText } from "../text-boundary.js";
|
|
4
4
|
export const TOOL_RESULT_CLIP_MARKER = "[tool output clipped]";
|
|
5
5
|
const FAIR_CONTENT_CODE_UNITS = 256;
|
|
6
|
-
const
|
|
7
|
-
const MAX_TOOL_RESULT_PROJECTION_CODE_UNITS = 256_000;
|
|
6
|
+
const MAX_TOOL_RESULT_PROJECTION_CODE_UNITS = 1_000_000;
|
|
8
7
|
export function toolResultProjectionBudget(policy) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* Keep historical result excerpts byte-for-byte stable as new results append.
|
|
13
|
-
* Semantic compaction is allowed to establish a new prefix before the safety
|
|
14
|
-
* fallback below redistributes an exhausted aggregate budget.
|
|
15
|
-
*/
|
|
16
|
-
export function projectToolResults(source, requestedCodeUnits) {
|
|
17
|
-
const results = toolResults(source, requestedCodeUnits);
|
|
18
|
-
if (results.length === 0) {
|
|
19
|
-
return { messages: [...source], clippedResults: 0, outputCodeUnits: 0, saturated: false };
|
|
20
|
-
}
|
|
21
|
-
const allocations = [];
|
|
22
|
-
let remaining = requestedCodeUnits;
|
|
23
|
-
let saturated = false;
|
|
24
|
-
for (const result of results) {
|
|
25
|
-
const desired = Math.min(result.output.length, APPEND_STABLE_RESULT_CODE_UNITS);
|
|
26
|
-
if (desired <= remaining) {
|
|
27
|
-
allocations.push(desired);
|
|
28
|
-
remaining -= desired;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
saturated = true;
|
|
32
|
-
const excerpt = remaining >= TOOL_RESULT_CLIP_MARKER.length ? remaining : 0;
|
|
33
|
-
allocations.push(Math.min(result.output.length, excerpt));
|
|
34
|
-
remaining -= excerpt;
|
|
35
|
-
}
|
|
36
|
-
return projectAllocations(source, allocations, saturated);
|
|
8
|
+
// Character allocation is only a starting point. The actual provider input
|
|
9
|
+
// is measured again before it can be sent, including Unicode and framing.
|
|
10
|
+
return Math.min(MAX_TOOL_RESULT_PROJECTION_CODE_UNITS, policy.targetTokens * 3);
|
|
37
11
|
}
|
|
38
12
|
/** Prefer recent evidence when compaction cannot restore a stable prefix. */
|
|
39
13
|
export function projectToolResultsNewest(source, requestedCodeUnits) {
|
|
40
14
|
const results = toolResults(source, requestedCodeUnits);
|
|
41
15
|
if (results.length === 0) {
|
|
42
|
-
return { messages: [...source], clippedResults: 0, outputCodeUnits: 0
|
|
16
|
+
return { messages: [...source], clippedResults: 0, outputCodeUnits: 0 };
|
|
43
17
|
}
|
|
44
18
|
const allocations = results.map(() => 0);
|
|
45
19
|
let remaining = requestedCodeUnits;
|
|
@@ -58,7 +32,7 @@ export function projectToolResultsNewest(source, requestedCodeUnits) {
|
|
|
58
32
|
allocations[index] = allocations[index] + extra;
|
|
59
33
|
remaining -= extra;
|
|
60
34
|
}
|
|
61
|
-
return projectAllocations(source, allocations
|
|
35
|
+
return projectAllocations(source, allocations);
|
|
62
36
|
}
|
|
63
37
|
function toolResults(source, requestedCodeUnits) {
|
|
64
38
|
if (!Number.isSafeInteger(requestedCodeUnits) || requestedCodeUnits < 0) {
|
|
@@ -66,7 +40,7 @@ function toolResults(source, requestedCodeUnits) {
|
|
|
66
40
|
}
|
|
67
41
|
return source.flatMap((message) => message.content.filter((block) => block.kind === "tool_result"));
|
|
68
42
|
}
|
|
69
|
-
function projectAllocations(source, allocations
|
|
43
|
+
function projectAllocations(source, allocations) {
|
|
70
44
|
let resultIndex = 0;
|
|
71
45
|
let clippedResults = 0;
|
|
72
46
|
let outputCodeUnits = 0;
|
|
@@ -86,7 +60,7 @@ function projectAllocations(source, allocations, saturated) {
|
|
|
86
60
|
});
|
|
87
61
|
return changed ? { ...message, content } : message;
|
|
88
62
|
});
|
|
89
|
-
return { messages, clippedResults, outputCodeUnits
|
|
63
|
+
return { messages, clippedResults, outputCodeUnits };
|
|
90
64
|
}
|
|
91
65
|
function allocateFairExcerpt(results, allocations, available) {
|
|
92
66
|
const needs = results.map((result, index) => {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Complete ordinary input; bounded tool projection is an emergency fallback.
|
|
2
|
+
import { MIN_REQUEST_OUTPUT_TOKENS } from "./policy.js";
|
|
3
|
+
import { projectToolResultsNewest, toolResultProjectionBudget } from "./request-projection.js";
|
|
4
|
+
/** Called after optional semantic compaction, immediately before sending. */
|
|
5
|
+
export async function fitRequestInput(input, meter, policy, measurement, signal) {
|
|
6
|
+
const limit = policy.requestLimitTokens - MIN_REQUEST_OUTPUT_TOKENS;
|
|
7
|
+
if (measurement.inputTokens <= limit) {
|
|
8
|
+
return { messages: [...input.messages], measurement, clippedResults: 0 };
|
|
9
|
+
}
|
|
10
|
+
let budget = toolResultProjectionBudget(policy);
|
|
11
|
+
for (;;) {
|
|
12
|
+
signal?.throwIfAborted();
|
|
13
|
+
const projection = projectToolResultsNewest(input.messages, budget);
|
|
14
|
+
const next = await meter.measure({ ...input, messages: projection.messages }, signal);
|
|
15
|
+
if (next.inputTokens <= limit || budget === 0 || projection.clippedResults === 0) {
|
|
16
|
+
return { messages: projection.messages, measurement: next, clippedResults: projection.clippedResults };
|
|
17
|
+
}
|
|
18
|
+
// At most logarithmically many bounded attempts, then the existing request
|
|
19
|
+
// guard reports an irreducible prompt/schema instead of a provider call.
|
|
20
|
+
budget = budget < 256 ? 0 : Math.floor(budget / 2);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function countPiece(bytes, ranks) {
|
|
2
|
+
const text = bytes.toString("latin1");
|
|
3
|
+
if (ranks.has(text))
|
|
4
|
+
return 1;
|
|
5
|
+
const next = Int32Array.from({ length: bytes.length }, (_, i) => i + 1);
|
|
6
|
+
const previous = Int32Array.from({ length: bytes.length }, (_, i) => i - 1);
|
|
7
|
+
const heap = [];
|
|
8
|
+
let count = bytes.length;
|
|
9
|
+
const offer = (left) => {
|
|
10
|
+
if (left < 0 || left >= bytes.length)
|
|
11
|
+
return;
|
|
12
|
+
const right = next[left];
|
|
13
|
+
if (right < 0 || right >= bytes.length)
|
|
14
|
+
return;
|
|
15
|
+
const end = next[right];
|
|
16
|
+
const rank = ranks.get(text.slice(left, end));
|
|
17
|
+
if (rank !== undefined)
|
|
18
|
+
push(heap, { rank, left, right, end });
|
|
19
|
+
};
|
|
20
|
+
for (let i = 0; i + 1 < bytes.length; i++)
|
|
21
|
+
offer(i);
|
|
22
|
+
while (heap.length > 0) {
|
|
23
|
+
const pair = pop(heap);
|
|
24
|
+
if (next[pair.left] !== pair.right || next[pair.right] !== pair.end)
|
|
25
|
+
continue;
|
|
26
|
+
next[pair.left] = pair.end;
|
|
27
|
+
next[pair.right] = -1;
|
|
28
|
+
if (pair.end < bytes.length)
|
|
29
|
+
previous[pair.end] = pair.left;
|
|
30
|
+
count--;
|
|
31
|
+
offer(previous[pair.left]);
|
|
32
|
+
offer(pair.left);
|
|
33
|
+
}
|
|
34
|
+
return count;
|
|
35
|
+
}
|
|
36
|
+
function before(a, b) {
|
|
37
|
+
return a.rank < b.rank || (a.rank === b.rank && a.left < b.left);
|
|
38
|
+
}
|
|
39
|
+
function push(heap, pair) {
|
|
40
|
+
let index = heap.length;
|
|
41
|
+
heap.push(pair);
|
|
42
|
+
while (index > 0) {
|
|
43
|
+
const parent = (index - 1) >>> 1;
|
|
44
|
+
if (!before(pair, heap[parent]))
|
|
45
|
+
break;
|
|
46
|
+
heap[index] = heap[parent];
|
|
47
|
+
index = parent;
|
|
48
|
+
}
|
|
49
|
+
heap[index] = pair;
|
|
50
|
+
}
|
|
51
|
+
function pop(heap) {
|
|
52
|
+
const first = heap[0];
|
|
53
|
+
const tail = heap.pop();
|
|
54
|
+
if (heap.length === 0)
|
|
55
|
+
return first;
|
|
56
|
+
let index = 0;
|
|
57
|
+
while (index * 2 + 1 < heap.length) {
|
|
58
|
+
let child = index * 2 + 1;
|
|
59
|
+
if (child + 1 < heap.length && before(heap[child + 1], heap[child]))
|
|
60
|
+
child++;
|
|
61
|
+
if (!before(heap[child], tail))
|
|
62
|
+
break;
|
|
63
|
+
heap[index] = heap[child];
|
|
64
|
+
index = child;
|
|
65
|
+
}
|
|
66
|
+
heap[index] = tail;
|
|
67
|
+
return first;
|
|
68
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Owned o200k_base ordinary-text counter. Special-looking user text stays literal.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { countPiece } from "./bpe.js";
|
|
4
|
+
import { vocabulary } from "./vocabulary.js";
|
|
5
|
+
// OpenAI tiktoken's o200k_base split, with scoped ASCII case-insensitivity
|
|
6
|
+
// expanded for JavaScript. Vocabulary and expression attribution: assets/tokenizers/LICENSE.
|
|
7
|
+
const pattern = /[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]*[\p{Ll}\p{Lm}\p{Lo}\p{M}]+(?:'[sStTmMdD]|'[rR][eE]|'[vV][eE]|'[lL][lL])?|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]+[\p{Ll}\p{Lm}\p{Lo}\p{M}]*(?:'[sStTmMdD]|'[rR][eE]|'[vV][eE]|'[lL][lL])?|\p{N}{1,3}| ?[^\p{White_Space}\p{L}\p{N}]+[\r\n/]*|\p{White_Space}*[\r\n]+|\p{White_Space}+(?!\P{White_Space})|\p{White_Space}+/gu;
|
|
8
|
+
const cache = new Map();
|
|
9
|
+
const CHUNK = 8_192;
|
|
10
|
+
export async function countO200k(text, signal) {
|
|
11
|
+
signal?.throwIfAborted();
|
|
12
|
+
const key = createHash("sha256").update(text).digest("hex");
|
|
13
|
+
const cached = cache.get(key);
|
|
14
|
+
if (cached !== undefined)
|
|
15
|
+
return cached;
|
|
16
|
+
const ranks = await vocabulary();
|
|
17
|
+
signal?.throwIfAborted();
|
|
18
|
+
let count = 0;
|
|
19
|
+
// Repetitive logs or minified data must not repeat the same merge work.
|
|
20
|
+
// Keep text only within this call, bounded independently of input size.
|
|
21
|
+
const chunks = new Map();
|
|
22
|
+
let chunkUnits = 0;
|
|
23
|
+
for (let start = 0; start < text.length;) {
|
|
24
|
+
let end = Math.min(start + CHUNK, text.length);
|
|
25
|
+
if (end < text.length && /[\uD800-\uDBFF]/u.test(text[end - 1]))
|
|
26
|
+
end--;
|
|
27
|
+
const chunk = text.slice(start, end);
|
|
28
|
+
let tokens = chunks.get(chunk);
|
|
29
|
+
if (tokens === undefined) {
|
|
30
|
+
tokens = 0;
|
|
31
|
+
for (const match of chunk.matchAll(pattern))
|
|
32
|
+
tokens += countPiece(Buffer.from(match[0], "utf8"), ranks);
|
|
33
|
+
while (chunkUnits + chunk.length > 131_072) {
|
|
34
|
+
const oldest = chunks.keys().next().value;
|
|
35
|
+
chunks.delete(oldest);
|
|
36
|
+
chunkUnits -= oldest.length;
|
|
37
|
+
}
|
|
38
|
+
chunks.set(chunk, tokens);
|
|
39
|
+
chunkUnits += chunk.length;
|
|
40
|
+
}
|
|
41
|
+
count += tokens;
|
|
42
|
+
// Bounded slices may split a token/pre-token. Reserve boundary room; this
|
|
43
|
+
// is an input estimate, not a claim of exact whole-request tokenization.
|
|
44
|
+
if (end < text.length)
|
|
45
|
+
count += 8;
|
|
46
|
+
start = end;
|
|
47
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
48
|
+
signal?.throwIfAborted();
|
|
49
|
+
}
|
|
50
|
+
if (cache.size >= 2_048)
|
|
51
|
+
cache.delete(cache.keys().next().value);
|
|
52
|
+
cache.set(key, count);
|
|
53
|
+
return count;
|
|
54
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Pinned data, loaded once on demand. No network, executable data, or user cache.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { gunzip } from "node:zlib";
|
|
6
|
+
let loading;
|
|
7
|
+
const unpack = promisify(gunzip);
|
|
8
|
+
const digest = "446a9538cb6c348e3516120d7c08b09f57c36495e2acfffe59a5bf8b0cfb1a2d";
|
|
9
|
+
export function vocabulary() {
|
|
10
|
+
loading ??= load();
|
|
11
|
+
return loading;
|
|
12
|
+
}
|
|
13
|
+
async function load() {
|
|
14
|
+
const compressed = await readFile(new URL("../../../assets/tokenizers/o200k-base.tiktoken.gz", import.meta.url));
|
|
15
|
+
if (compressed.length > 2_000_000)
|
|
16
|
+
throw new Error("token vocabulary exceeds its size limit");
|
|
17
|
+
const data = await unpack(compressed, { maxOutputLength: 4_000_000 });
|
|
18
|
+
if (createHash("sha256").update(data).digest("hex") !== digest) {
|
|
19
|
+
throw new Error("token vocabulary checksum mismatch");
|
|
20
|
+
}
|
|
21
|
+
const ranks = new Map();
|
|
22
|
+
const lines = data.toString("ascii").trimEnd().split("\n");
|
|
23
|
+
for (let index = 0; index < lines.length; index++) {
|
|
24
|
+
const [encoded, rank] = lines[index].split(" ");
|
|
25
|
+
if (encoded === undefined || Number(rank) !== index)
|
|
26
|
+
throw new Error("invalid token vocabulary");
|
|
27
|
+
ranks.set(Buffer.from(encoded, "base64").toString("latin1"), index);
|
|
28
|
+
if (index % 2_048 === 0)
|
|
29
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
30
|
+
}
|
|
31
|
+
if (ranks.size !== 199_998)
|
|
32
|
+
throw new Error("incomplete token vocabulary");
|
|
33
|
+
return ranks;
|
|
34
|
+
}
|