@danypops/jittor 0.12.0 → 0.13.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 +3 -2
- package/src/cli-commands/service-daemon.ts +33 -24
- package/src/cli-commands/support.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/client.ts +2 -2
- package/src/constants.ts +18 -0
- package/src/daemon.ts +8 -8
- package/src/db.ts +2 -2
- package/src/domain/context-hub.ts +226 -0
- package/src/index.ts +14 -0
- package/src/log.ts +3 -3
- package/src/ports/session-identity-store.ts +2 -2
- package/src/service.ts +2 -2
- package/src/session-identity-service.ts +1 -1
- package/src/state.ts +10 -10
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/jittor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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,8 @@
|
|
|
16
16
|
"service:install": "bun src/cli.ts service install"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@danypops/
|
|
19
|
+
"@danypops/vehicle-server": "^0.3.1",
|
|
20
|
+
"@danypops/vehicle-client": "^0.1.1",
|
|
20
21
|
"google-auth-library": "^10.9.0"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
@@ -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().systemdUnit;
|
|
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/cli.ts
CHANGED
|
@@ -52,7 +52,7 @@ function usage(stderr: (line: string) => void): number {
|
|
|
52
52
|
export async function runCli(args: string[], deps: CliDependencies = DEFAULT_DEPENDENCIES): Promise<number> {
|
|
53
53
|
const [command, action, ...rest] = args;
|
|
54
54
|
const fail = () => usage(deps.stderr);
|
|
55
|
-
if (command === "serve") { deps.serve(); return 0; }
|
|
55
|
+
if (command === "serve") { await deps.serve(); return 0; }
|
|
56
56
|
if (command === "session") return runSessionCommand(action, rest, deps, fail);
|
|
57
57
|
if (command === "metrics") return runMetricsCommand(action, rest, deps, fail);
|
|
58
58
|
if (command === "telemetry") return runTelemetryCommand(action, rest, deps, fail);
|
package/src/client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthenticatedRpcClient, type FetchTransport } from "@danypops/
|
|
1
|
+
import { AuthenticatedRpcClient, type FetchTransport } from "@danypops/vehicle-client/rpc-client";
|
|
2
2
|
import type { OperationInputs, OperationName, OperationOutputs } from "./service.ts";
|
|
3
3
|
import { ensureAuthToken, readDaemonHandle, resolveJittorPaths, type JittorPaths } from "./state.ts";
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ export type { FetchTransport };
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Jittor's typed authenticated RPC client, now a thin named subclass of
|
|
9
|
-
* `@danypops/
|
|
9
|
+
* `@danypops/vehicle-client/rpc-client`'s `AuthenticatedRpcClient` -- the shared substrate factored
|
|
10
10
|
* out after jittor's own client.ts and web-spider-daemon's were found byte-identical (see
|
|
11
11
|
* daemon-kit's README). Keeps the old 3-positional-argument constructor so every existing call
|
|
12
12
|
* site is untouched by this migration.
|
package/src/constants.ts
CHANGED
|
@@ -115,3 +115,21 @@ 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
|
+
export const CONTEXT_HUB_CONFIDENCE_TIERS = ["exact-tool", "exact-cooperative", "correlated", "audited"] as const;
|
package/src/daemon.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { startDaemon as startDaemonKit, type RunningDaemon } from "@danypops/
|
|
1
|
+
import { startDaemon as startDaemonKit, type RunningDaemon } from "@danypops/vehicle-server/daemon";
|
|
2
2
|
import { MAINTENANCE_INTERVAL_MS, TELEMETRY_POLL_INTERVAL_MS } from "./constants.ts";
|
|
3
3
|
import { DEFAULT_POLICY, UNCONFIGURED_ROUTE } from "./config.ts";
|
|
4
4
|
import { SQLiteMetricStore } from "./adapters/sqlite-metric-store.ts";
|
|
@@ -23,7 +23,7 @@ import type { GoogleVertexMetricSource } from "./providers/google-vertex-contrac
|
|
|
23
23
|
import { ensureAuthToken, resolveJittorPaths, type JittorPaths } from "./state.ts";
|
|
24
24
|
import { logEvent, logger } from "./log.ts";
|
|
25
25
|
|
|
26
|
-
export type { RunningDaemon } from "@danypops/
|
|
26
|
+
export type { RunningDaemon } from "@danypops/vehicle-server/daemon";
|
|
27
27
|
|
|
28
28
|
export function reportMaintenanceFailure(event: string, error: unknown): void {
|
|
29
29
|
logEvent("error", event, { message: error instanceof Error ? error.message : String(error) });
|
|
@@ -65,7 +65,7 @@ export function telemetrySourcesFromEnvironment(env: Record<string, string | und
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* Composition root, now built on `@danypops/
|
|
68
|
+
* Composition root, now built on `@danypops/vehicle-server/daemon`'s `startDaemon` for binding,
|
|
69
69
|
* atomic handle write, maintenance-timer driving, and clean shutdown -- the skeleton that used to
|
|
70
70
|
* be hand-rolled here (and, byte-identically, in web-spider-daemon's and papyrus's daemon.ts; see
|
|
71
71
|
* daemon-kit's README). Each maintenance task still catches and classifies its own failure via
|
|
@@ -74,10 +74,10 @@ export function telemetrySourcesFromEnvironment(env: Record<string, string | und
|
|
|
74
74
|
* daemon-kit's own generic "maintenance task failed: <name>" catch, which exists as a safety net
|
|
75
75
|
* for tasks that don't self-classify, not to replace a consumer's own richer classification.
|
|
76
76
|
*/
|
|
77
|
-
export function startDaemon(
|
|
77
|
+
export async function startDaemon(
|
|
78
78
|
paths: JittorPaths = resolveJittorPaths(),
|
|
79
79
|
env: Record<string, string | undefined> = process.env,
|
|
80
|
-
): RunningDaemon {
|
|
80
|
+
): Promise<RunningDaemon> {
|
|
81
81
|
const token = ensureAuthToken(paths);
|
|
82
82
|
const db = openJittorDb(paths.database);
|
|
83
83
|
const metrics = new SQLiteMetricStore(db);
|
|
@@ -96,7 +96,7 @@ export function startDaemon(
|
|
|
96
96
|
});
|
|
97
97
|
const service = new JittorService(metrics, router, benchmarks, modelRanker, sessionIdentity);
|
|
98
98
|
|
|
99
|
-
const daemon = startDaemonKit({
|
|
99
|
+
const daemon = await startDaemonKit({
|
|
100
100
|
daemonLabel: "Jittor",
|
|
101
101
|
handlePath: paths.handle,
|
|
102
102
|
logger,
|
|
@@ -115,8 +115,8 @@ export function startDaemon(
|
|
|
115
115
|
return daemon;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
export function serveMain(): void {
|
|
119
|
-
const daemon = startDaemon();
|
|
118
|
+
export async function serveMain(): Promise<void> {
|
|
119
|
+
const daemon = await startDaemon();
|
|
120
120
|
console.error(`[jittor] listening on ${daemon.host}:${daemon.port}`);
|
|
121
121
|
const stop = async (): Promise<void> => {
|
|
122
122
|
await daemon.stop();
|
package/src/db.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
-
import { openSqliteWithPragmas } from "@danypops/
|
|
2
|
+
import { openSqliteWithPragmas } from "@danypops/vehicle-server/storage";
|
|
3
3
|
import { SQLITE_BUSY_TIMEOUT_MS } from "./constants.ts";
|
|
4
4
|
|
|
5
5
|
const INITIAL_SCHEMA = `
|
|
@@ -31,7 +31,7 @@ CREATE INDEX session_identities_last_seen_idx
|
|
|
31
31
|
`;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Delegates bootstrap (pragmas, migration engine) to `@danypops/
|
|
34
|
+
* Delegates bootstrap (pragmas, migration engine) to `@danypops/vehicle-server/storage`, which
|
|
35
35
|
* generalizes the byte-identical pragma/PRAGMA-user_version skeleton jittor's own db.ts used to
|
|
36
36
|
* hand-roll (see daemon-kit's README). Jittor's only remaining responsibility is its own schema.
|
|
37
37
|
*/
|
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
|
|
44
|
+
export interface ContextContribution {
|
|
45
|
+
schema: typeof CONTEXT_HUB_CONTRIBUTION_SCHEMA;
|
|
46
|
+
observedAt: number;
|
|
47
|
+
sequence: number;
|
|
48
|
+
producerName: string;
|
|
49
|
+
segment: ContextSegment;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function nonEmptyString(value: unknown, name: string, maxLength: number): string {
|
|
53
|
+
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`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function boundedInteger(value: unknown, name: string): number {
|
|
58
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`${name} must be a bounded non-negative integer`);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function countItems(items: ContextSegmentItem[]): number {
|
|
63
|
+
return items.reduce((sum, item) => sum + 1 + (item.children ? countItems(item.children) : 0), 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateSegmentItem(value: unknown, depth: number): ContextSegmentItem {
|
|
67
|
+
if (depth > CONTEXT_HUB_MAX_ITEM_DEPTH) throw new Error(`context segment item nesting exceeds ${CONTEXT_HUB_MAX_ITEM_DEPTH} levels`);
|
|
68
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context segment item must be an object");
|
|
69
|
+
const input = value as Record<string, unknown>;
|
|
70
|
+
for (const key of Object.keys(input)) {
|
|
71
|
+
if (key !== "label" && key !== "estimatedTokens" && key !== "children") throw new Error(`context segment item contains unexpected field: ${key}`);
|
|
72
|
+
}
|
|
73
|
+
const item: ContextSegmentItem = {
|
|
74
|
+
label: nonEmptyString(input["label"], "item.label", CONTEXT_HUB_ITEM_LABEL_MAX_CHARACTERS),
|
|
75
|
+
estimatedTokens: boundedInteger(input["estimatedTokens"], "item.estimatedTokens"),
|
|
76
|
+
};
|
|
77
|
+
if (input["children"] !== undefined) {
|
|
78
|
+
if (!Array.isArray(input["children"])) throw new Error("item.children must be an array");
|
|
79
|
+
item.children = input["children"].map((child) => validateSegmentItem(child, depth + 1));
|
|
80
|
+
}
|
|
81
|
+
return item;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Validates a bare segment (no contribution envelope) -- shared by validateContextContribution and Jittor's own directly-computed segments (tool ledger, base prompt, ...). */
|
|
85
|
+
export function validateContextSegment(value: unknown): ContextSegment {
|
|
86
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context segment must be an object");
|
|
87
|
+
const input = value as Record<string, unknown>;
|
|
88
|
+
for (const key of Object.keys(input)) {
|
|
89
|
+
if (key !== "key" && key !== "label" && key !== "estimatedTokens" && key !== "confidence" && key !== "items") {
|
|
90
|
+
throw new Error(`context segment contains unexpected field: ${key}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const confidence = input["confidence"];
|
|
94
|
+
if (typeof confidence !== "string" || !CONTEXT_HUB_CONFIDENCE_TIERS.includes(confidence as ContextConfidenceTier)) {
|
|
95
|
+
throw new Error(`confidence must be one of ${CONTEXT_HUB_CONFIDENCE_TIERS.join(", ")}`);
|
|
96
|
+
}
|
|
97
|
+
const segment: ContextSegment = {
|
|
98
|
+
key: nonEmptyString(input["key"], "segment.key", CONTEXT_HUB_SEGMENT_KEY_MAX_CHARACTERS),
|
|
99
|
+
label: nonEmptyString(input["label"], "segment.label", CONTEXT_HUB_SEGMENT_LABEL_MAX_CHARACTERS),
|
|
100
|
+
estimatedTokens: boundedInteger(input["estimatedTokens"], "segment.estimatedTokens"),
|
|
101
|
+
confidence: confidence as ContextConfidenceTier,
|
|
102
|
+
};
|
|
103
|
+
if (input["items"] !== undefined) {
|
|
104
|
+
if (!Array.isArray(input["items"])) throw new Error("segment.items must be an array");
|
|
105
|
+
const items = input["items"].map((item) => validateSegmentItem(item, 1));
|
|
106
|
+
if (countItems(items) > CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT) throw new Error(`segment.items exceeds ${CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT} total items`);
|
|
107
|
+
segment.items = items;
|
|
108
|
+
}
|
|
109
|
+
return segment;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const CONTRIBUTION_FIELDS = new Set(["schema", "observedAt", "sequence", "producerName", "segment"]);
|
|
113
|
+
|
|
114
|
+
/** Validates one contribution posted on CONTEXT_HUB_CONTRIBUTION_CHANNEL. Fails closed on schema drift or an oversized/malformed payload, the same posture as validatePapyrusContextInjection. */
|
|
115
|
+
export function validateContextContribution(value: unknown, now = Date.now()): ContextContribution {
|
|
116
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context contribution must be an object");
|
|
117
|
+
const input = value as Record<string, unknown>;
|
|
118
|
+
for (const key of Object.keys(input)) if (!CONTRIBUTION_FIELDS.has(key)) throw new Error(`context contribution contains unexpected field: ${key}`);
|
|
119
|
+
if (input["schema"] !== CONTEXT_HUB_CONTRIBUTION_SCHEMA) throw new Error("context contribution schema is not supported");
|
|
120
|
+
const observedAt = boundedInteger(input["observedAt"], "observedAt");
|
|
121
|
+
if (Math.abs(now - observedAt) > CONTEXT_HUB_CONTRIBUTION_MAX_AGE_MS) throw new Error("context contribution is stale");
|
|
122
|
+
return {
|
|
123
|
+
schema: CONTEXT_HUB_CONTRIBUTION_SCHEMA,
|
|
124
|
+
observedAt,
|
|
125
|
+
sequence: boundedInteger(input["sequence"], "sequence"),
|
|
126
|
+
producerName: nonEmptyString(input["producerName"], "producerName", CONTEXT_HUB_PRODUCER_NAME_MAX_CHARACTERS),
|
|
127
|
+
segment: validateContextSegment(input["segment"]),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Bounded, content-free metric projection for storage/history -- mirrors papyrusContextMetric's shape. */
|
|
132
|
+
export function contextContributionMetric(contribution: ContextContribution): MetricObservation {
|
|
133
|
+
return {
|
|
134
|
+
source: "context-hub",
|
|
135
|
+
scope: contribution.producerName,
|
|
136
|
+
metric: "segment-tokens",
|
|
137
|
+
value: contribution.segment.estimatedTokens,
|
|
138
|
+
unit: "count",
|
|
139
|
+
observedAt: contribution.observedAt,
|
|
140
|
+
attributes: {
|
|
141
|
+
sequence: contribution.sequence,
|
|
142
|
+
segmentKey: contribution.segment.key,
|
|
143
|
+
segmentLabel: contribution.segment.label,
|
|
144
|
+
confidence: contribution.segment.confidence,
|
|
145
|
+
itemCount: contribution.segment.items ? countItems(contribution.segment.items) : 0,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Exact, zero-cooperation attribution of tool-schema cost: every registered tool's `sourceInfo`
|
|
152
|
+
* (populated by Pi's own resource loader from the extension that registered it, never guessed)
|
|
153
|
+
* identifies its owning extension. Tool schemas are fixed overhead paid on every single turn
|
|
154
|
+
* regardless of whether the tool is ever called, so this is the walking-skeleton signal --
|
|
155
|
+
* buildable without any other package changing.
|
|
156
|
+
*/
|
|
157
|
+
export interface ToolLedgerEntry {
|
|
158
|
+
name: string;
|
|
159
|
+
description?: string;
|
|
160
|
+
parameters?: unknown;
|
|
161
|
+
promptGuidelines?: string[];
|
|
162
|
+
promptSnippet?: string;
|
|
163
|
+
sourceInfo?: { source?: string; path?: string };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface ToolLedgerToolUsage {
|
|
167
|
+
name: string;
|
|
168
|
+
characters: number;
|
|
169
|
+
estimatedTokens: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface ToolLedgerSourceUsage {
|
|
173
|
+
source: string;
|
|
174
|
+
toolCount: number;
|
|
175
|
+
characters: number;
|
|
176
|
+
estimatedTokens: number;
|
|
177
|
+
tools: ToolLedgerToolUsage[];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function toolCharacters(tool: ToolLedgerEntry): number {
|
|
181
|
+
const parameterCharacters = tool.parameters === undefined ? 0 : JSON.stringify(tool.parameters).length;
|
|
182
|
+
const guidelineCharacters = (tool.promptGuidelines ?? []).reduce((sum, guideline) => sum + guideline.length, 0);
|
|
183
|
+
return tool.name.length + (tool.description?.length ?? 0) + parameterCharacters + guidelineCharacters + (tool.promptSnippet?.length ?? 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 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. */
|
|
187
|
+
export function computeToolSchemaLedger(tools: readonly ToolLedgerEntry[]): ToolLedgerSourceUsage[] {
|
|
188
|
+
const bySource = new Map<string, ToolLedgerToolUsage[]>();
|
|
189
|
+
for (const tool of tools) {
|
|
190
|
+
const source = tool.sourceInfo?.source && tool.sourceInfo.source.length > 0 ? tool.sourceInfo.source : "unknown";
|
|
191
|
+
const characters = toolCharacters(tool);
|
|
192
|
+
const usage: ToolLedgerToolUsage = { name: tool.name, characters, estimatedTokens: Math.ceil(characters / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN) };
|
|
193
|
+
const existing = bySource.get(source);
|
|
194
|
+
if (existing) existing.push(usage);
|
|
195
|
+
else bySource.set(source, [usage]);
|
|
196
|
+
}
|
|
197
|
+
return [...bySource.entries()]
|
|
198
|
+
.map(([source, toolUsages]) => {
|
|
199
|
+
const sorted = [...toolUsages].sort((left, right) => right.characters - left.characters);
|
|
200
|
+
return {
|
|
201
|
+
source,
|
|
202
|
+
toolCount: sorted.length,
|
|
203
|
+
characters: sorted.reduce((sum, tool) => sum + tool.characters, 0),
|
|
204
|
+
estimatedTokens: sorted.reduce((sum, tool) => sum + tool.estimatedTokens, 0),
|
|
205
|
+
tools: sorted,
|
|
206
|
+
};
|
|
207
|
+
})
|
|
208
|
+
.sort((left, right) => right.characters - left.characters);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Projects the tool-schema ledger into one ContextSegment (`toolDefinitions`), ready to merge alongside any contributed segment. */
|
|
212
|
+
export function toolLedgerSegment(tools: readonly ToolLedgerEntry[]): ContextSegment {
|
|
213
|
+
const ledger = computeToolSchemaLedger(tools);
|
|
214
|
+
const items: ContextSegmentItem[] = ledger.map((sourceUsage) => ({
|
|
215
|
+
label: `${sourceUsage.source} (${sourceUsage.toolCount} tool${sourceUsage.toolCount === 1 ? "" : "s"})`,
|
|
216
|
+
estimatedTokens: sourceUsage.estimatedTokens,
|
|
217
|
+
children: sourceUsage.tools.map((tool) => ({ label: tool.name, estimatedTokens: tool.estimatedTokens })),
|
|
218
|
+
}));
|
|
219
|
+
return {
|
|
220
|
+
key: "toolDefinitions",
|
|
221
|
+
label: "Tool definitions",
|
|
222
|
+
estimatedTokens: items.reduce((sum, item) => sum + item.estimatedTokens, 0),
|
|
223
|
+
confidence: "exact-tool",
|
|
224
|
+
items,
|
|
225
|
+
};
|
|
226
|
+
}
|
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,
|
package/src/log.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structured daemon logging, now backed by `@danypops/
|
|
2
|
+
* Structured daemon logging, now backed by `@danypops/vehicle-server/logging` (pino) instead of a
|
|
3
3
|
* hand-rolled `console.error(JSON.stringify(...))` -- daemon-kit's own module doc explains why:
|
|
4
4
|
* level ordering/filtering/child-scoping is exactly the kind of thing worth one shared,
|
|
5
5
|
* dependency-backed implementation instead of four independent hand-rolled ones. One deliberate,
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* four daemons. `component`/`level`/`timestamp` and credential-safety (callers still must pass
|
|
9
9
|
* only bounded, non-sensitive fields) are unchanged.
|
|
10
10
|
*/
|
|
11
|
-
import { createLogger, type LogLevel as
|
|
11
|
+
import { createLogger, type LogLevel as VehicleLogLevel, type Logger } from "@danypops/vehicle-server/logging";
|
|
12
12
|
|
|
13
|
-
export type LogLevel = Extract<
|
|
13
|
+
export type LogLevel = Extract<VehicleLogLevel, "info" | "warn" | "error">;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Also passed directly as `StartDaemonOptions.logger` so daemon-kit's own maintenance-task
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SessionIdentityRecord, SessionIdentityStore as
|
|
1
|
+
import type { SessionIdentityRecord, SessionIdentityStore as VehicleSessionIdentityStore } from "@danypops/vehicle-server/session-identity";
|
|
2
2
|
|
|
3
3
|
/** Jittor's persistence port for daemon-kit's storage-agnostic session-identity primitive. */
|
|
4
|
-
export type SessionIdentityStore =
|
|
4
|
+
export type SessionIdentityStore = VehicleSessionIdentityStore;
|
|
5
5
|
export type { SessionIdentityRecord };
|
package/src/service.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errorResponse, healthResponse, readyResponse, requireBearerToken } from "@danypops/
|
|
1
|
+
import { errorResponse, healthResponse, readyResponse, requireBearerToken } from "@danypops/vehicle-server/rpc-http";
|
|
2
2
|
import { SERVICE_MAX_BODY_BYTES, SERVICE_MAX_RESPONSE_BYTES } from "./constants.ts";
|
|
3
3
|
import { InvalidSessionSecretError, SessionIdentity, type RegisterSessionIdentityResult } from "./session-identity-service.ts";
|
|
4
4
|
import { VERSION } from "./version.ts";
|
|
@@ -188,7 +188,7 @@ export interface JittorAppOptions {
|
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
190
|
* Bearer-check and the trivial health/ready/not-found responses now delegate to
|
|
191
|
-
* `@danypops/
|
|
191
|
+
* `@danypops/vehicle-server/rpc-http` (the same handful of lines every daemon's service.ts hand-rolled).
|
|
192
192
|
* The response-size guard below stays jittor-specific: daemon-kit's `jsonResponse` is intentionally
|
|
193
193
|
* unbounded (it has no operation dispatch of its own to guard), while jittor's `/api/v1/ops` can
|
|
194
194
|
* return arbitrarily large query results that must be capped (see SERVICE_MAX_RESPONSE_BYTES).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isSessionRegistered, registerSessionIdentity, releaseSessionIdentity, verifySessionSecret } from "@danypops/
|
|
1
|
+
import { isSessionRegistered, registerSessionIdentity, releaseSessionIdentity, verifySessionSecret } from "@danypops/vehicle-server/session-identity";
|
|
2
2
|
import type { SessionIdentityStore } from "./ports/session-identity-store.ts";
|
|
3
3
|
|
|
4
4
|
export interface RegisterSessionIdentityResult {
|
package/src/state.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Jittor's XDG paths/token/handle layout, now delegating to `@danypops/
|
|
2
|
+
* Jittor's XDG paths/token/handle layout, now delegating to `@danypops/vehicle-server/paths` --
|
|
3
3
|
* the shared substrate factored out after jittor's own state.ts and web-spider-daemon's were
|
|
4
4
|
* found byte-identical (see daemon-kit's README). Kept as a thin jittor-named wrapper (same
|
|
5
5
|
* exported function names/signatures as before) so every existing call site (daemon.ts,
|
|
6
6
|
* client.ts, cli.ts, and their tests) is untouched by this migration.
|
|
7
7
|
*/
|
|
8
8
|
import {
|
|
9
|
-
ensureAuthToken as
|
|
10
|
-
readDaemonHandle as
|
|
11
|
-
removeDaemonHandle as
|
|
9
|
+
ensureAuthToken as ensureVehicleAuthToken,
|
|
10
|
+
readDaemonHandle as readVehicleHandle,
|
|
11
|
+
removeDaemonHandle as removeVehicleHandle,
|
|
12
12
|
resolveDaemonPaths,
|
|
13
|
-
writeDaemonHandle as
|
|
13
|
+
writeDaemonHandle as writeVehicleHandle,
|
|
14
14
|
type DaemonHandle,
|
|
15
15
|
type DaemonPaths,
|
|
16
16
|
type PathEnvironment,
|
|
17
|
-
} from "@danypops/
|
|
17
|
+
} from "@danypops/vehicle-server/paths";
|
|
18
18
|
import {
|
|
19
19
|
DATABASE_FILENAME,
|
|
20
20
|
HANDLE_FILENAME,
|
|
@@ -39,17 +39,17 @@ export function resolveJittorPaths(options: PathEnvironment = {}): JittorPaths {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function ensureAuthToken(paths: JittorPaths = resolveJittorPaths()): string {
|
|
42
|
-
return
|
|
42
|
+
return ensureVehicleAuthToken(paths.token, "Jittor");
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function writeDaemonHandle(paths: JittorPaths, handle: DaemonHandle): void {
|
|
46
|
-
|
|
46
|
+
writeVehicleHandle(paths.handle, handle);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function readDaemonHandle(paths: JittorPaths = resolveJittorPaths()): DaemonHandle | null {
|
|
50
|
-
return
|
|
50
|
+
return readVehicleHandle(paths.handle);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function removeDaemonHandle(paths: JittorPaths = resolveJittorPaths()): void {
|
|
54
|
-
|
|
54
|
+
removeVehicleHandle(paths.handle);
|
|
55
55
|
}
|
package/src/version.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readPackageVersion } from "@danypops/
|
|
1
|
+
import { readPackageVersion } from "@danypops/vehicle-server/version";
|
|
2
2
|
|
|
3
3
|
/** Runtime package version; package.json is the single release source of truth. */
|
|
4
4
|
export const VERSION = readPackageVersion(new URL("../package.json", import.meta.url), "Jittor");
|