@giovannijecha/jecode 0.8.5 → 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 +36 -9
- package/assets/tokenizers/LICENSE +21 -0
- package/assets/tokenizers/o200k-base.tiktoken.gz +0 -0
- package/dist/cli-info.js +8 -14
- package/dist/commands.js +7 -3
- package/dist/config.js +47 -53
- 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/credential-commands.js +3 -3
- 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-account-command.js +14 -12
- package/dist/openai-account.js +7 -5
- package/dist/openai-oauth-callback.js +15 -54
- package/dist/openai-oauth-tokens.js +9 -7
- package/dist/openai-oauth.js +8 -6
- package/dist/permission-command.js +9 -24
- package/dist/permissions.js +10 -8
- package/dist/provider-commands.js +1 -33
- package/dist/provider-errors.js +1 -10
- package/dist/provider-label.js +3 -10
- package/dist/providers/anthropic-wire.js +1 -1
- package/dist/providers/anthropic.js +3 -2
- package/dist/providers/index.js +1 -5
- package/dist/providers/input-measurement.js +85 -0
- package/dist/providers/ollama-context.js +42 -0
- package/dist/providers/ollama-endpoint.js +7 -34
- package/dist/providers/ollama-wire.js +1 -1
- package/dist/providers/ollama.js +15 -147
- package/dist/providers/openai-codex.js +7 -4
- 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 -1
- package/dist/sessions/bucket.js +55 -0
- package/dist/sessions/catalog-io.js +162 -0
- package/dist/sessions/catalog.js +3 -1
- package/dist/sessions/codec-messages.js +122 -0
- package/dist/sessions/codec-transcript.js +93 -0
- package/dist/sessions/codec-values.js +52 -0
- package/dist/sessions/codec.js +4 -257
- package/dist/sessions/files.js +158 -0
- package/dist/sessions/load.js +90 -0
- package/dist/sessions/snapshot.js +33 -0
- package/dist/sessions/store.js +42 -461
- package/dist/settings.js +17 -16
- package/dist/start.js +36 -53
- package/dist/timeline.js +2 -0
- package/dist/tools/file-read.js +192 -0
- package/dist/tools/file-summary.js +9 -0
- package/dist/tools/{fs.js → file-write.js} +5 -193
- package/dist/tools/glob.js +107 -0
- package/dist/tools/index.js +2 -1
- package/dist/tools/search.js +1 -105
- package/dist/tui/activity.js +1 -1
- package/dist/tui/app-input.js +34 -17
- package/dist/tui/app-workflows.js +13 -361
- package/dist/tui/app.js +55 -28
- package/dist/tui/approve.js +3 -2
- package/dist/tui/blocks.js +1 -2
- package/dist/tui/command-workflow.js +112 -0
- package/dist/tui/components/command-menu.js +7 -10
- package/dist/tui/components/menu.js +82 -43
- package/dist/tui/components/messages.js +16 -9
- package/dist/tui/components/status.js +1 -1
- package/dist/tui/components/tool-evidence.js +107 -0
- package/dist/tui/components/tool-motion.js +32 -0
- package/dist/tui/components/tool.js +48 -202
- package/dist/tui/frame.js +8 -3
- package/dist/tui/help.js +3 -2
- package/dist/tui/picker-layout.js +44 -0
- package/dist/tui/picker.js +7 -71
- package/dist/tui/screen.js +7 -0
- package/dist/tui/session-view.js +0 -1
- package/dist/tui/tool-details.js +135 -0
- package/dist/tui/transcript-grammar.js +1 -1
- package/dist/tui/transcript-view.js +30 -112
- package/dist/tui/turn-workflow.js +235 -0
- package/dist/tui/turn.js +7 -140
- package/dist/tui/view.js +4 -4
- package/dist/tui/workflow-types.js +2 -0
- package/dist/ui/render.js +0 -4
- package/package.json +16 -12
- package/dist/batch-view.js +0 -17
- package/dist/batch.js +0 -267
- package/dist/ollama-settings-command.js +0 -74
- package/dist/tui/motion.js +0 -32
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Opt-in development observations. Never publish text, identities, or raw data.
|
|
2
|
+
import { channel } from "node:diagnostics_channel";
|
|
3
|
+
export const CONTEXT_DIAGNOSTIC_CHANNEL = "jecode.context";
|
|
4
|
+
const observations = channel(CONTEXT_DIAGNOSTIC_CHANNEL);
|
|
5
|
+
/** Whitelist at the channel boundary; subscribers must not serialize callers. */
|
|
6
|
+
export function safeDiagnostic(value) {
|
|
7
|
+
if (typeof value !== "object" || value === null)
|
|
8
|
+
return undefined;
|
|
9
|
+
const v = value;
|
|
10
|
+
if (v["kind"] === "preparation" && (v["reason"] === "budget" || v["reason"] === "overflow") &&
|
|
11
|
+
(v["outcome"] === "failed" || v["outcome"] === "cancelled") && count(v["elapsedMs"]) &&
|
|
12
|
+
count(v["windowTokens"]) && count(v["triggerTokens"]) && count(v["requestLimitTokens"])) {
|
|
13
|
+
return { kind: "preparation", reason: v["reason"], outcome: v["outcome"], elapsedMs: v["elapsedMs"],
|
|
14
|
+
windowTokens: v["windowTokens"], triggerTokens: v["triggerTokens"], requestLimitTokens: v["requestLimitTokens"] };
|
|
15
|
+
}
|
|
16
|
+
if (v["kind"] === "request" &&
|
|
17
|
+
(v["source"] === "estimate" || v["source"] === "provider-prefix") &&
|
|
18
|
+
count(v["estimatedTokens"]) && count(v["inputTokens"]) &&
|
|
19
|
+
(v["reportedInputTokens"] === undefined || count(v["reportedInputTokens"]))) {
|
|
20
|
+
const extras = {};
|
|
21
|
+
for (const key of ["windowTokens", "triggerTokens", "requestLimitTokens", "outputBudgetTokens",
|
|
22
|
+
"preparationMs", "providerMs", "firstEventMs", "clippedResults"]) {
|
|
23
|
+
if (v[key] !== undefined) {
|
|
24
|
+
if (!count(v[key]))
|
|
25
|
+
return undefined;
|
|
26
|
+
Object.assign(extras, { [key]: v[key] });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const result = v["outcome"];
|
|
30
|
+
if (result !== undefined && result !== "completed" && result !== "failed" && result !== "cancelled")
|
|
31
|
+
return undefined;
|
|
32
|
+
const tokenization = v["tokenization"];
|
|
33
|
+
if (tokenization !== undefined && tokenization !== "o200k-reference" && tokenization !== "heuristic")
|
|
34
|
+
return undefined;
|
|
35
|
+
return { ...extras, ...(result === undefined ? {} : { outcome: result }),
|
|
36
|
+
...(tokenization === undefined ? {} : { tokenization }),
|
|
37
|
+
kind: "request", source: v["source"], estimatedTokens: v["estimatedTokens"],
|
|
38
|
+
inputTokens: v["inputTokens"],
|
|
39
|
+
...(v["reportedInputTokens"] === undefined ? {} : { reportedInputTokens: v["reportedInputTokens"] }) };
|
|
40
|
+
}
|
|
41
|
+
if (v["kind"] === "compaction" &&
|
|
42
|
+
(v["reason"] === "budget" || v["reason"] === "overflow" || v["reason"] === "manual") &&
|
|
43
|
+
outcome(v["outcome"]) && count(v["beforeTokens"]) && count(v["elapsedMs"]) &&
|
|
44
|
+
(v["afterTokens"] === undefined || count(v["afterTokens"]))) {
|
|
45
|
+
const limits = {};
|
|
46
|
+
for (const key of ["windowTokens", "triggerTokens", "requestLimitTokens",
|
|
47
|
+
"summaryChars", "summaryProviderMs", "firstSummaryTextMs"]) {
|
|
48
|
+
if (v[key] !== undefined) {
|
|
49
|
+
if (!count(v[key]))
|
|
50
|
+
return undefined;
|
|
51
|
+
limits[key] = v[key];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { ...limits, kind: "compaction", reason: v["reason"], outcome: v["outcome"],
|
|
55
|
+
beforeTokens: v["beforeTokens"], elapsedMs: v["elapsedMs"],
|
|
56
|
+
...(v["afterTokens"] === undefined ? {} : { afterTokens: v["afterTokens"] }) };
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
export function publishDiagnostic(event) {
|
|
61
|
+
if (!observations.hasSubscribers)
|
|
62
|
+
return;
|
|
63
|
+
const safe = safeDiagnostic(event);
|
|
64
|
+
if (safe !== undefined)
|
|
65
|
+
observations.publish(safe);
|
|
66
|
+
}
|
|
67
|
+
export async function observedCompaction(options, perform) {
|
|
68
|
+
const started = performance.now();
|
|
69
|
+
let state = "no-prefix";
|
|
70
|
+
let result;
|
|
71
|
+
let summary;
|
|
72
|
+
try {
|
|
73
|
+
result = await perform({ ...options,
|
|
74
|
+
onBegin() { state = "failed"; options.onBegin?.(); },
|
|
75
|
+
onOutcome(value) { state = value; options.onOutcome?.(value); },
|
|
76
|
+
onSummary(value) { summary = value; options.onSummary?.(value); },
|
|
77
|
+
});
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (options.signal?.aborted)
|
|
82
|
+
state = "cancelled";
|
|
83
|
+
else if (state === "no-prefix")
|
|
84
|
+
state = "failed";
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
const event = {
|
|
89
|
+
kind: "compaction", reason: options.reason ?? (options.force ? "manual" : "budget"),
|
|
90
|
+
outcome: state, beforeTokens: options.estimatedInputTokens ?? 0,
|
|
91
|
+
windowTokens: options.policy.windowTokens, triggerTokens: options.policy.triggerTokens,
|
|
92
|
+
requestLimitTokens: options.policy.requestLimitTokens,
|
|
93
|
+
...(result === undefined ? {} : { afterTokens: result.estimatedInputTokens }),
|
|
94
|
+
elapsedMs: Math.round(performance.now() - started),
|
|
95
|
+
...summary,
|
|
96
|
+
};
|
|
97
|
+
publishDiagnostic(event);
|
|
98
|
+
options.onDiagnostic?.(event);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function count(value) {
|
|
102
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
103
|
+
}
|
|
104
|
+
function outcome(value) {
|
|
105
|
+
return value === "accepted" || value === "empty" || value === "oversized" ||
|
|
106
|
+
value === "insufficient-savings" || value === "failed" || value === "timeout" ||
|
|
107
|
+
value === "cancelled" || value === "no-prefix";
|
|
108
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Reuse only the current conversation's meter. No calibration survives restart.
|
|
2
|
+
import { inputMeter } from "./measurement.js";
|
|
3
|
+
export function inputLifetime() {
|
|
4
|
+
let current;
|
|
5
|
+
return {
|
|
6
|
+
forTurn(provider, conversation) {
|
|
7
|
+
if (current?.provider !== provider || current.conversation !== conversation) {
|
|
8
|
+
current?.meter.reset();
|
|
9
|
+
current = { provider, conversation, meter: inputMeter(provider) };
|
|
10
|
+
}
|
|
11
|
+
return current.meter;
|
|
12
|
+
},
|
|
13
|
+
reset() {
|
|
14
|
+
current?.meter.reset();
|
|
15
|
+
current = undefined;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -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
|
+
}
|