@danypops/jittor 0.12.1 → 0.14.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/package.json +2 -2
- package/src/cli-commands/service-daemon.ts +33 -24
- package/src/constants.ts +22 -0
- package/src/domain/context-hub.ts +236 -0
- package/src/index.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/jittor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Just-in-Time Token Optimizing Router for Pi -- supervised daemon, router policy, and CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"service:install": "bun src/cli.ts service install"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@danypops/vehicle-server": "^0.
|
|
19
|
+
"@danypops/vehicle-server": "^0.3.1",
|
|
20
20
|
"@danypops/vehicle-client": "^0.1.1",
|
|
21
21
|
"google-auth-library": "^10.9.0"
|
|
22
22
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createNodeServiceInstallDeps, generateSystemdUnit, installUserService, type ServiceSpec } from "@danypops/vehicle-server/service";
|
|
5
6
|
import { SYSTEMD_UNIT_NAME } from "../constants.ts";
|
|
6
7
|
import { resolveJittorPaths } from "../state.ts";
|
|
7
8
|
import { callAndPrint, type CliDependencies } from "./support.ts";
|
|
@@ -16,23 +17,30 @@ export interface SystemdUnitOptions {
|
|
|
16
17
|
openRouterBenchmarks?: boolean;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
function jittorServiceSpec(options: SystemdUnitOptions): ServiceSpec {
|
|
21
|
+
const env: Record<string, string> = {};
|
|
22
|
+
if (options.codexAuthFile) env["JITTOR_CODEX_AUTH_FILE"] = options.codexAuthFile;
|
|
23
|
+
if (options.openRouterBenchmarks) env["JITTOR_OPENROUTER_BENCHMARKS"] = "1";
|
|
24
|
+
return {
|
|
25
|
+
name: "jittor",
|
|
26
|
+
displayName: "Jittor token optimizing router",
|
|
27
|
+
binPath: options.bunBin,
|
|
28
|
+
args: [options.cliPath, "serve"],
|
|
29
|
+
env,
|
|
30
|
+
descriptorPath: resolveJittorPaths().serviceDescriptor,
|
|
31
|
+
// Jittor's own client (connectJittorClient) never auto-spawns -- systemd's own
|
|
32
|
+
// supervision is this daemon's only recovery path, same as Lector's.
|
|
33
|
+
restartOnFailure: true,
|
|
34
|
+
restartSec: 2,
|
|
35
|
+
noNewPrivileges: true,
|
|
36
|
+
privateTmp: true,
|
|
37
|
+
waitForNetwork: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
/** Pure text generator, delegating to vehicle-server's shared generateSystemdUnit -- kept as its own named export since jittor's own tests (and any external caller) call it directly with the same options shape as before. */
|
|
42
|
+
export function renderSystemdUnit(options: SystemdUnitOptions): string {
|
|
43
|
+
return generateSystemdUnit(jittorServiceSpec(options));
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
export function systemctl(...args: string[]): void {
|
|
@@ -41,17 +49,18 @@ export function systemctl(...args: string[]): void {
|
|
|
41
49
|
|
|
42
50
|
/** cliPath is the caller's own entrypoint file -- resolved from the real CLI script's `import.meta.url`, never this module's own, so the installed unit's ExecStart always points at the actual runnable CLI. */
|
|
43
51
|
export function installService(cliPath: string): void {
|
|
44
|
-
const unitPath = resolveJittorPaths().serviceDescriptor;
|
|
45
|
-
mkdirSync(dirname(unitPath), { recursive: true });
|
|
46
52
|
const codexAuthFile = join(process.env["CODEX_HOME"] ?? join(homedir(), ".codex"), "auth.json");
|
|
47
|
-
|
|
53
|
+
const spec = jittorServiceSpec({
|
|
48
54
|
bunBin: process.execPath,
|
|
49
55
|
cliPath,
|
|
50
56
|
...(existsSync(codexAuthFile) ? { codexAuthFile } : {}),
|
|
51
57
|
openRouterBenchmarks: process.env["JITTOR_OPENROUTER_BENCHMARKS"] === "1",
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
});
|
|
59
|
+
const result = installUserService(spec, createNodeServiceInstallDeps());
|
|
60
|
+
if (!result.installed) throw new Error(`failed to install the Jittor service: ${result.reason}`);
|
|
61
|
+
// installUserService's Linux path is `enable --now` (starts if not already running) --
|
|
62
|
+
// an explicit restart on top ensures a re-install after a Jittor upgrade actually picks
|
|
63
|
+
// up the freshly-generated unit's new ExecStart path, not just re-enables the old one.
|
|
55
64
|
systemctl("restart", SYSTEMD_UNIT_NAME);
|
|
56
65
|
}
|
|
57
66
|
|
package/src/constants.ts
CHANGED
|
@@ -115,3 +115,25 @@ export const GOOGLE_VERTEX_BUDGET_MAX_MESSAGES_PER_PULL = 20;
|
|
|
115
115
|
*/
|
|
116
116
|
export const GOOGLE_VERTEX_BUDGET_CONFIDENCE = 0.6;
|
|
117
117
|
export const GOOGLE_ADC_TOKEN_REFRESH_SKEW_MS = 60_000;
|
|
118
|
+
/**
|
|
119
|
+
* Context Hub: the shared, versioned, multi-producer channel any extension can target to
|
|
120
|
+
* contribute one segment of the context-window breakdown (successor to the single-producer
|
|
121
|
+
* papyrus.context-injection.v1 shape) -- Papyrus's rules/tasks segment is the first producer.
|
|
122
|
+
*/
|
|
123
|
+
export const CONTEXT_HUB_CONTRIBUTION_CHANNEL = "jittor.context-contribution.v1";
|
|
124
|
+
export const CONTEXT_HUB_CONTRIBUTION_SCHEMA = "jittor.context-contribution/v1";
|
|
125
|
+
export const CONTEXT_HUB_PRODUCER_NAME_MAX_CHARACTERS = 80;
|
|
126
|
+
export const CONTEXT_HUB_SEGMENT_LABEL_MAX_CHARACTERS = 160;
|
|
127
|
+
export const CONTEXT_HUB_SEGMENT_KEY_MAX_CHARACTERS = 80;
|
|
128
|
+
export const CONTEXT_HUB_ITEM_LABEL_MAX_CHARACTERS = 200;
|
|
129
|
+
export const CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT = 500;
|
|
130
|
+
export const CONTEXT_HUB_MAX_ITEM_DEPTH = 6;
|
|
131
|
+
export const CONTEXT_HUB_CONTRIBUTION_MAX_AGE_MS = 5 * MILLISECONDS_PER_MINUTE;
|
|
132
|
+
export const CONTEXT_HUB_CONTRIBUTION_DEDUP_LIMIT = 1_000;
|
|
133
|
+
/** Matches Papyrus's own CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN; kept independent since Jittor does not depend on the Papyrus package. */
|
|
134
|
+
export const CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN = 4;
|
|
135
|
+
/** Matches Papyrus's own CONTEXT_TREE_MAX_NODES: a bound on the message-history tree walk, independent of any other producer's own item-count bound (CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT). */
|
|
136
|
+
export const CONTEXT_TREE_MAX_NODES = 50_000;
|
|
137
|
+
/** Pi's own documented compaction-reserve default (docs/compaction.md): headroom kept free for the model's response, subtracted from the model's contextWindow to get the real usable budget. */
|
|
138
|
+
export const CONTEXT_DEFAULT_RESERVE_TOKENS = 16_384;
|
|
139
|
+
export const CONTEXT_HUB_CONFIDENCE_TIERS = ["exact-tool", "exact-structural", "exact-cooperative", "correlated", "audited"] as const;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Hub: Jittor's cross-extension context-window attribution. Two independent pieces:
|
|
3
|
+
*
|
|
4
|
+
* - The tool-schema ledger (computeToolSchemaLedger/toolLedgerSegment): exact, zero-cooperation
|
|
5
|
+
* attribution of tool-schema cost (name + description + parameters + guidelines) per owning
|
|
6
|
+
* extension, using Pi's own `sourceInfo` on every registered tool -- no other package needs to
|
|
7
|
+
* change for this to work.
|
|
8
|
+
* - The shared contribution channel (validateContextContribution/contextContributionMetric): any
|
|
9
|
+
* extension can opt in and contribute one segment of the breakdown (e.g. Papyrus's rules/tasks
|
|
10
|
+
* segment) over `CONTEXT_HUB_CONTRIBUTION_CHANNEL`, the same shared-bus pattern already proven
|
|
11
|
+
* by papyrus.context-injection.v1, generalized to a human-readable producer name and a nested
|
|
12
|
+
* segment/item shape instead of one flat character count.
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN,
|
|
16
|
+
CONTEXT_HUB_CONFIDENCE_TIERS,
|
|
17
|
+
CONTEXT_HUB_CONTRIBUTION_MAX_AGE_MS,
|
|
18
|
+
CONTEXT_HUB_CONTRIBUTION_SCHEMA,
|
|
19
|
+
CONTEXT_HUB_ITEM_LABEL_MAX_CHARACTERS,
|
|
20
|
+
CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT,
|
|
21
|
+
CONTEXT_HUB_MAX_ITEM_DEPTH,
|
|
22
|
+
CONTEXT_HUB_PRODUCER_NAME_MAX_CHARACTERS,
|
|
23
|
+
CONTEXT_HUB_SEGMENT_KEY_MAX_CHARACTERS,
|
|
24
|
+
CONTEXT_HUB_SEGMENT_LABEL_MAX_CHARACTERS,
|
|
25
|
+
} from "../constants.ts";
|
|
26
|
+
import type { MetricObservation } from "./metric.ts";
|
|
27
|
+
|
|
28
|
+
export type ContextConfidenceTier = (typeof CONTEXT_HUB_CONFIDENCE_TIERS)[number];
|
|
29
|
+
|
|
30
|
+
export interface ContextSegmentItem {
|
|
31
|
+
label: string;
|
|
32
|
+
estimatedTokens: number;
|
|
33
|
+
children?: ContextSegmentItem[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ContextSegment {
|
|
37
|
+
key: string;
|
|
38
|
+
label: string;
|
|
39
|
+
estimatedTokens: number;
|
|
40
|
+
confidence: ContextConfidenceTier;
|
|
41
|
+
items?: ContextSegmentItem[];
|
|
42
|
+
/**
|
|
43
|
+
* True when this segment's size is genuinely unmeasured (not yet observed), as opposed to
|
|
44
|
+
* measured-and-actually-zero -- e.g. the base system prompt before the first observed turn.
|
|
45
|
+
* A display layer that hides zero-token rows to cut noise must NOT hide an unknown segment
|
|
46
|
+
* just because its placeholder value happens to be zero -- that would silently misrepresent
|
|
47
|
+
* "we don't know" as "there is nothing here".
|
|
48
|
+
*/
|
|
49
|
+
unknown?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ContextContribution {
|
|
53
|
+
schema: typeof CONTEXT_HUB_CONTRIBUTION_SCHEMA;
|
|
54
|
+
observedAt: number;
|
|
55
|
+
sequence: number;
|
|
56
|
+
producerName: string;
|
|
57
|
+
segment: ContextSegment;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function nonEmptyString(value: unknown, name: string, maxLength: number): string {
|
|
61
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength) throw new Error(`${name} must be a non-empty string of at most ${maxLength} characters`);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function boundedInteger(value: unknown, name: string): number {
|
|
66
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`${name} must be a bounded non-negative integer`);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function countItems(items: ContextSegmentItem[]): number {
|
|
71
|
+
return items.reduce((sum, item) => sum + 1 + (item.children ? countItems(item.children) : 0), 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validateSegmentItem(value: unknown, depth: number): ContextSegmentItem {
|
|
75
|
+
if (depth > CONTEXT_HUB_MAX_ITEM_DEPTH) throw new Error(`context segment item nesting exceeds ${CONTEXT_HUB_MAX_ITEM_DEPTH} levels`);
|
|
76
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context segment item must be an object");
|
|
77
|
+
const input = value as Record<string, unknown>;
|
|
78
|
+
for (const key of Object.keys(input)) {
|
|
79
|
+
if (key !== "label" && key !== "estimatedTokens" && key !== "children") throw new Error(`context segment item contains unexpected field: ${key}`);
|
|
80
|
+
}
|
|
81
|
+
const item: ContextSegmentItem = {
|
|
82
|
+
label: nonEmptyString(input["label"], "item.label", CONTEXT_HUB_ITEM_LABEL_MAX_CHARACTERS),
|
|
83
|
+
estimatedTokens: boundedInteger(input["estimatedTokens"], "item.estimatedTokens"),
|
|
84
|
+
};
|
|
85
|
+
if (input["children"] !== undefined) {
|
|
86
|
+
if (!Array.isArray(input["children"])) throw new Error("item.children must be an array");
|
|
87
|
+
item.children = input["children"].map((child) => validateSegmentItem(child, depth + 1));
|
|
88
|
+
}
|
|
89
|
+
return item;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Validates a bare segment (no contribution envelope) -- shared by validateContextContribution and Jittor's own directly-computed segments (tool ledger, base prompt, ...). */
|
|
93
|
+
export function validateContextSegment(value: unknown): ContextSegment {
|
|
94
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context segment must be an object");
|
|
95
|
+
const input = value as Record<string, unknown>;
|
|
96
|
+
for (const key of Object.keys(input)) {
|
|
97
|
+
if (key !== "key" && key !== "label" && key !== "estimatedTokens" && key !== "confidence" && key !== "items" && key !== "unknown") {
|
|
98
|
+
throw new Error(`context segment contains unexpected field: ${key}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const confidence = input["confidence"];
|
|
102
|
+
if (typeof confidence !== "string" || !CONTEXT_HUB_CONFIDENCE_TIERS.includes(confidence as ContextConfidenceTier)) {
|
|
103
|
+
throw new Error(`confidence must be one of ${CONTEXT_HUB_CONFIDENCE_TIERS.join(", ")}`);
|
|
104
|
+
}
|
|
105
|
+
if (input["unknown"] !== undefined && typeof input["unknown"] !== "boolean") throw new Error("segment.unknown must be a boolean");
|
|
106
|
+
const segment: ContextSegment = {
|
|
107
|
+
key: nonEmptyString(input["key"], "segment.key", CONTEXT_HUB_SEGMENT_KEY_MAX_CHARACTERS),
|
|
108
|
+
label: nonEmptyString(input["label"], "segment.label", CONTEXT_HUB_SEGMENT_LABEL_MAX_CHARACTERS),
|
|
109
|
+
estimatedTokens: boundedInteger(input["estimatedTokens"], "segment.estimatedTokens"),
|
|
110
|
+
confidence: confidence as ContextConfidenceTier,
|
|
111
|
+
...(input["unknown"] !== undefined ? { unknown: input["unknown"] as boolean } : {}),
|
|
112
|
+
};
|
|
113
|
+
if (input["items"] !== undefined) {
|
|
114
|
+
if (!Array.isArray(input["items"])) throw new Error("segment.items must be an array");
|
|
115
|
+
const items = input["items"].map((item) => validateSegmentItem(item, 1));
|
|
116
|
+
if (countItems(items) > CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT) throw new Error(`segment.items exceeds ${CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT} total items`);
|
|
117
|
+
segment.items = items;
|
|
118
|
+
}
|
|
119
|
+
return segment;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const CONTRIBUTION_FIELDS = new Set(["schema", "observedAt", "sequence", "producerName", "segment"]);
|
|
123
|
+
|
|
124
|
+
/** Validates one contribution posted on CONTEXT_HUB_CONTRIBUTION_CHANNEL. Fails closed on schema drift or an oversized/malformed payload, the same posture as validatePapyrusContextInjection. */
|
|
125
|
+
export function validateContextContribution(value: unknown, now = Date.now()): ContextContribution {
|
|
126
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context contribution must be an object");
|
|
127
|
+
const input = value as Record<string, unknown>;
|
|
128
|
+
for (const key of Object.keys(input)) if (!CONTRIBUTION_FIELDS.has(key)) throw new Error(`context contribution contains unexpected field: ${key}`);
|
|
129
|
+
if (input["schema"] !== CONTEXT_HUB_CONTRIBUTION_SCHEMA) throw new Error("context contribution schema is not supported");
|
|
130
|
+
const observedAt = boundedInteger(input["observedAt"], "observedAt");
|
|
131
|
+
if (Math.abs(now - observedAt) > CONTEXT_HUB_CONTRIBUTION_MAX_AGE_MS) throw new Error("context contribution is stale");
|
|
132
|
+
return {
|
|
133
|
+
schema: CONTEXT_HUB_CONTRIBUTION_SCHEMA,
|
|
134
|
+
observedAt,
|
|
135
|
+
sequence: boundedInteger(input["sequence"], "sequence"),
|
|
136
|
+
producerName: nonEmptyString(input["producerName"], "producerName", CONTEXT_HUB_PRODUCER_NAME_MAX_CHARACTERS),
|
|
137
|
+
segment: validateContextSegment(input["segment"]),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Bounded, content-free metric projection for storage/history -- mirrors papyrusContextMetric's shape. */
|
|
142
|
+
export function contextContributionMetric(contribution: ContextContribution): MetricObservation {
|
|
143
|
+
return {
|
|
144
|
+
source: "context-hub",
|
|
145
|
+
scope: contribution.producerName,
|
|
146
|
+
metric: "segment-tokens",
|
|
147
|
+
value: contribution.segment.estimatedTokens,
|
|
148
|
+
unit: "count",
|
|
149
|
+
observedAt: contribution.observedAt,
|
|
150
|
+
attributes: {
|
|
151
|
+
sequence: contribution.sequence,
|
|
152
|
+
segmentKey: contribution.segment.key,
|
|
153
|
+
segmentLabel: contribution.segment.label,
|
|
154
|
+
confidence: contribution.segment.confidence,
|
|
155
|
+
itemCount: contribution.segment.items ? countItems(contribution.segment.items) : 0,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Exact, zero-cooperation attribution of tool-schema cost: every registered tool's `sourceInfo`
|
|
162
|
+
* (populated by Pi's own resource loader from the extension that registered it, never guessed)
|
|
163
|
+
* identifies its owning extension. Tool schemas are fixed overhead paid on every single turn
|
|
164
|
+
* regardless of whether the tool is ever called, so this is the walking-skeleton signal --
|
|
165
|
+
* buildable without any other package changing.
|
|
166
|
+
*/
|
|
167
|
+
export interface ToolLedgerEntry {
|
|
168
|
+
name: string;
|
|
169
|
+
description?: string;
|
|
170
|
+
parameters?: unknown;
|
|
171
|
+
promptGuidelines?: string[];
|
|
172
|
+
promptSnippet?: string;
|
|
173
|
+
sourceInfo?: { source?: string; path?: string };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface ToolLedgerToolUsage {
|
|
177
|
+
name: string;
|
|
178
|
+
characters: number;
|
|
179
|
+
estimatedTokens: number;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface ToolLedgerSourceUsage {
|
|
183
|
+
source: string;
|
|
184
|
+
toolCount: number;
|
|
185
|
+
characters: number;
|
|
186
|
+
estimatedTokens: number;
|
|
187
|
+
tools: ToolLedgerToolUsage[];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function toolCharacters(tool: ToolLedgerEntry): number {
|
|
191
|
+
const parameterCharacters = tool.parameters === undefined ? 0 : JSON.stringify(tool.parameters).length;
|
|
192
|
+
const guidelineCharacters = (tool.promptGuidelines ?? []).reduce((sum, guideline) => sum + guideline.length, 0);
|
|
193
|
+
return tool.name.length + (tool.description?.length ?? 0) + parameterCharacters + guidelineCharacters + (tool.promptSnippet?.length ?? 0);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Groups every registered tool's serialized schema size by its owning extension (`sourceInfo.source`), sorted heaviest-first at both the source and tool level. A tool with no sourceInfo (e.g. an SDK-supplied custom tool) is grouped under "unknown" rather than dropped. */
|
|
197
|
+
export function computeToolSchemaLedger(tools: readonly ToolLedgerEntry[]): ToolLedgerSourceUsage[] {
|
|
198
|
+
const bySource = new Map<string, ToolLedgerToolUsage[]>();
|
|
199
|
+
for (const tool of tools) {
|
|
200
|
+
const source = tool.sourceInfo?.source && tool.sourceInfo.source.length > 0 ? tool.sourceInfo.source : "unknown";
|
|
201
|
+
const characters = toolCharacters(tool);
|
|
202
|
+
const usage: ToolLedgerToolUsage = { name: tool.name, characters, estimatedTokens: Math.ceil(characters / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN) };
|
|
203
|
+
const existing = bySource.get(source);
|
|
204
|
+
if (existing) existing.push(usage);
|
|
205
|
+
else bySource.set(source, [usage]);
|
|
206
|
+
}
|
|
207
|
+
return [...bySource.entries()]
|
|
208
|
+
.map(([source, toolUsages]) => {
|
|
209
|
+
const sorted = [...toolUsages].sort((left, right) => right.characters - left.characters);
|
|
210
|
+
return {
|
|
211
|
+
source,
|
|
212
|
+
toolCount: sorted.length,
|
|
213
|
+
characters: sorted.reduce((sum, tool) => sum + tool.characters, 0),
|
|
214
|
+
estimatedTokens: sorted.reduce((sum, tool) => sum + tool.estimatedTokens, 0),
|
|
215
|
+
tools: sorted,
|
|
216
|
+
};
|
|
217
|
+
})
|
|
218
|
+
.sort((left, right) => right.characters - left.characters);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Projects the tool-schema ledger into one ContextSegment (`toolDefinitions`), ready to merge alongside any contributed segment. */
|
|
222
|
+
export function toolLedgerSegment(tools: readonly ToolLedgerEntry[]): ContextSegment {
|
|
223
|
+
const ledger = computeToolSchemaLedger(tools);
|
|
224
|
+
const items: ContextSegmentItem[] = ledger.map((sourceUsage) => ({
|
|
225
|
+
label: `${sourceUsage.source} (${sourceUsage.toolCount} tool${sourceUsage.toolCount === 1 ? "" : "s"})`,
|
|
226
|
+
estimatedTokens: sourceUsage.estimatedTokens,
|
|
227
|
+
children: sourceUsage.tools.map((tool) => ({ label: tool.name, estimatedTokens: tool.estimatedTokens })),
|
|
228
|
+
}));
|
|
229
|
+
return {
|
|
230
|
+
key: "toolDefinitions",
|
|
231
|
+
label: "Tool definitions",
|
|
232
|
+
estimatedTokens: items.reduce((sum, item) => sum + item.estimatedTokens, 0),
|
|
233
|
+
confidence: "exact-tool",
|
|
234
|
+
items,
|
|
235
|
+
};
|
|
236
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,20 @@ export {
|
|
|
32
32
|
applyTaskFocusEvent,
|
|
33
33
|
validateTaskFocusEvent,
|
|
34
34
|
} from "./domain/task-focus.ts";
|
|
35
|
+
export {
|
|
36
|
+
type ContextConfidenceTier,
|
|
37
|
+
type ContextContribution,
|
|
38
|
+
type ContextSegment,
|
|
39
|
+
type ContextSegmentItem,
|
|
40
|
+
type ToolLedgerEntry,
|
|
41
|
+
type ToolLedgerSourceUsage,
|
|
42
|
+
type ToolLedgerToolUsage,
|
|
43
|
+
computeToolSchemaLedger,
|
|
44
|
+
contextContributionMetric,
|
|
45
|
+
toolLedgerSegment,
|
|
46
|
+
validateContextContribution,
|
|
47
|
+
validateContextSegment,
|
|
48
|
+
} from "./domain/context-hub.ts";
|
|
35
49
|
export {
|
|
36
50
|
METRIC_UNITS,
|
|
37
51
|
type MetricObservation,
|