afucontext 0.0.0-stage → 0.1.17

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.
Files changed (32) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +158 -2
  3. package/README.zh-CN.md +159 -0
  4. package/bin/afu.mjs +127 -0
  5. package/bin/cache-lease.mjs +16 -0
  6. package/bin/host-autoload.py +546 -0
  7. package/bin/install-guide.mjs +56 -0
  8. package/bin/install-process.mjs +19 -0
  9. package/bin/platform.mjs +74 -0
  10. package/bin/prebuilt.mjs +64 -0
  11. package/bin/runtime.mjs +115 -0
  12. package/package.json +29 -4
  13. package/platform-dependencies.json +15 -0
  14. package/platform-locks/dsh/package-lock.json +425 -0
  15. package/platform-locks/dsh/package.json +16 -0
  16. package/platform-locks/pi/package-lock.json +1789 -0
  17. package/platform-locks/pi/package.json +9 -0
  18. package/python/afu_context_local-0.1.17-py3-none-any.whl +0 -0
  19. package/python/constraints.txt +25 -0
  20. package/release.json +7 -0
  21. package/scripts/dsh-overlay.mjs +105 -0
  22. package/scripts/dsh-presets.mjs +49 -0
  23. package/scripts/dsh-web-probe.mjs +14 -0
  24. package/src/afu_host_integrations/dsh/dist/commands.js +28 -0
  25. package/src/afu_host_integrations/dsh/dist/index.js +548 -0
  26. package/src/afu_host_integrations/pi/dist/index.js +565 -0
  27. package/src/afu_host_integrations/shared/runtime-client/dist/commands.js +40 -0
  28. package/src/afu_host_integrations/shared/runtime-client/dist/host-worker.js +170 -0
  29. package/src/afu_host_integrations/shared/runtime-client/dist/index.js +243 -0
  30. package/src/afu_host_integrations/shared/runtime-client/dist/prefix.js +12 -0
  31. package/src/afu_host_integrations/shared/runtime-client/dist/protocol.js +79 -0
  32. package/src/afu_host_integrations/shared/runtime-client/dist/telemetry.js +78 -0
@@ -0,0 +1,170 @@
1
+ import { journal } from "./telemetry.js";
2
+ import { createServer } from "node:http";
3
+ import { createHash, randomBytes } from "node:crypto";
4
+ import { CompactWorkerClient, workerOptionsFromEnv } from "./index.js";
5
+ export function bindingKey(value) {
6
+ return createHash("sha256").update((JSON.stringify(value) ?? "null")).digest("hex");
7
+ }
8
+ /** Private in-process callback: provider credentials stay inside the host. */
9
+ export class HostCompactPool {
10
+ options;
11
+ server;
12
+ starting;
13
+ base = "";
14
+ capability = randomBytes(32).toString("hex");
15
+ entries = new Map();
16
+ controllers = new Set();
17
+ closed = false;
18
+ constructor(options = workerOptionsFromEnv()) {
19
+ this.options = options;
20
+ }
21
+ async compact(request, binding, signal) {
22
+ const override = this.options.env?.AFU_HOST_CONTEXT_WINDOW_OVERRIDE ?? process.env.AFU_HOST_CONTEXT_WINDOW_OVERRIDE;
23
+ if (override && override !== "auto") {
24
+ const window = Number(override);
25
+ if (!Number.isSafeInteger(window) || window < 1)
26
+ throw new Error("Invalid context window");
27
+ binding = { ...binding, contextWindow: window, key: bindingKey([binding.key, window]) };
28
+ }
29
+ if (this.closed || !await binding.current())
30
+ throw new Error("Host model changed");
31
+ this.starting ??= this.start();
32
+ await this.starting;
33
+ if (this.closed)
34
+ throw new Error("Host worker closed");
35
+ for (const [key, old] of this.entries) {
36
+ let active = true;
37
+ if (key !== binding.key) {
38
+ try {
39
+ active = await old.binding.current();
40
+ }
41
+ catch {
42
+ active = false;
43
+ }
44
+ }
45
+ if (!active) {
46
+ this.entries.delete(key);
47
+ const closing = old.client.close({ drain: false }).catch(() => undefined);
48
+ if (process.platform === "win32")
49
+ await closing;
50
+ }
51
+ }
52
+ if (this.closed)
53
+ throw new Error("Host worker closed");
54
+ let entry = this.entries.get(binding.key);
55
+ if (!entry) {
56
+ // Do not evict or interrupt another active session to admit a new one.
57
+ if (this.entries.size >= 16)
58
+ throw new Error("Host model binding limit reached");
59
+ const args = [...(this.options.args ?? ["compact", "worker"])];
60
+ const factory = args.indexOf("--factory");
61
+ if (factory >= 0 && args[factory + 1] !== "afu_context.local_worker:create_plugin")
62
+ throw new Error("Host binding requires the local worker factory");
63
+ if (factory < 0)
64
+ args.push("--factory", "afu_context.local_worker:create_plugin");
65
+ // Above the 600 s background summary limit, so the worker reports its own timeout.
66
+ const client = new CompactWorkerClient({ ...this.options, timeoutMs: this.options.timeoutMs ?? 630_000, args, env: {
67
+ ...this.options.env,
68
+ ...(binding.contextWindow === undefined ? {} : { AFU_CONTEXT_WINDOW: String(binding.contextWindow) }),
69
+ AFU_HOST_SUMMARY_URL: `${this.base}/${this.capability}/${binding.key}`,
70
+ AFU_HOST_SUMMARY_TOKEN: this.capability,
71
+ NO_PROXY: [process.env.NO_PROXY, "127.0.0.1"].filter(Boolean).join(","),
72
+ no_proxy: [process.env.no_proxy, "127.0.0.1"].filter(Boolean).join(","),
73
+ }, onStderr: () => undefined });
74
+ entry = { binding, client };
75
+ this.entries.set(binding.key, entry);
76
+ }
77
+ entry.binding = binding;
78
+ if (this.closed)
79
+ throw new Error("Host worker closed");
80
+ const result = await entry.client.compact({ ...request, ...(binding.contextWindow === undefined ? {} : { token_budget: binding.contextWindow }), session_id: bindingKey([request.session_id, binding.key]) }, signal);
81
+ if (!await binding.current())
82
+ throw new Error("Host model changed");
83
+ return result;
84
+ }
85
+ async start() {
86
+ const server = createServer(async (req, res) => {
87
+ const key = req.url?.split("/")[2];
88
+ const entry = key ? this.entries.get(key) : undefined;
89
+ // COSEC: loopback alone is not authentication; check a random capability,
90
+ // exact route and method. Never log provider errors, headers or bodies.
91
+ if (req.method !== "POST" || !entry || req.url !== `/${this.capability}/${key}/chat/completions` || req.headers.authorization !== `Bearer ${this.capability}`) {
92
+ res.writeHead(403).end();
93
+ return;
94
+ }
95
+ let artifactId;
96
+ let started;
97
+ const controller = new AbortController();
98
+ this.controllers.add(controller);
99
+ const timer = setTimeout(() => controller.abort(), 120_000);
100
+ res.on("close", () => { if (!res.writableEnded)
101
+ controller.abort(); });
102
+ try {
103
+ const chunks = [];
104
+ let size = 0;
105
+ for await (const chunk of req) {
106
+ size += chunk.length;
107
+ if (size > 16 * 1024 * 1024)
108
+ throw new Error("Summary too large");
109
+ chunks.push(chunk);
110
+ }
111
+ const body = JSON.parse(Buffer.concat(chunks).toString());
112
+ const prompt = body?.messages?.[0]?.content;
113
+ if (typeof prompt !== "string" || !await entry.binding.current())
114
+ throw new Error("Invalid summary request");
115
+ const diag = entry.binding.diagnostics;
116
+ artifactId = typeof body.afu_summary_id === "string" && /^[a-f0-9]{24}$/.test(body.afu_summary_id) ? body.afu_summary_id : undefined;
117
+ const ids = body.afu_source_item_ids;
118
+ // COSEC: source coverage is validated before any host-provider call.
119
+ if (ids !== undefined && (!Array.isArray(ids) || ids.some((id) => typeof id !== "string") || new Set(ids).size !== ids.length))
120
+ throw new Error("Invalid source coverage");
121
+ const binding = entry.binding;
122
+ started = performance.now();
123
+ if (diag)
124
+ journal(diag.host)("summary_started", { session: diag.session, artifact_id: artifactId });
125
+ const text = await binding.complete(prompt, controller.signal, ids);
126
+ if (!text.trim() || !await binding.current())
127
+ throw new Error("Stale summary");
128
+ if (diag)
129
+ journal(diag.host)("summary_ready", { session: diag.session, artifact_id: artifactId, duration_ms: performance.now() - started, ...diag.usage?.() });
130
+ res.setHeader("Content-Type", "application/json");
131
+ res.end(JSON.stringify({ choices: [{ message: { content: text }, finish_reason: "stop" }] }));
132
+ }
133
+ catch (error) {
134
+ const diag = entry.binding.diagnostics;
135
+ // A short, fixed reason code from the binding (never provider text).
136
+ const code = typeof error?.afuReason === "string" ? error.afuReason : "host_summary_unavailable";
137
+ if (diag)
138
+ journal(diag.host)("summary_failed", { session: diag.session, artifact_id: artifactId, duration_ms: started === undefined ? undefined : performance.now() - started, reason: controller.signal.aborted ? "summary_cancelled" : code });
139
+ if (!res.destroyed)
140
+ res.writeHead(502).end('{"error":"host_summary_unavailable"}');
141
+ }
142
+ finally {
143
+ clearTimeout(timer);
144
+ this.controllers.delete(controller);
145
+ }
146
+ });
147
+ this.server = server;
148
+ await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); });
149
+ const address = server.address();
150
+ if (!address || typeof address === "string")
151
+ throw new Error("Host bridge unavailable");
152
+ this.base = `http://127.0.0.1:${address.port}`;
153
+ }
154
+ async close() {
155
+ this.closed = true;
156
+ await this.starting?.catch(() => undefined);
157
+ for (const controller of this.controllers)
158
+ controller.abort();
159
+ // Entries registered by a compact() racing with close are closed here too.
160
+ while (this.entries.size) {
161
+ const entries = [...this.entries.values()];
162
+ this.entries.clear();
163
+ await Promise.allSettled(entries.map(e => e.client.close({ drain: false })));
164
+ }
165
+ this.server?.closeAllConnections();
166
+ if (this.server)
167
+ await new Promise(resolve => this.server.close(() => resolve()));
168
+ }
169
+ }
170
+ //# sourceMappingURL=host-worker.js.map
@@ -0,0 +1,243 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+ export * from "./protocol.js";
4
+ export class CompactWorkerError extends Error {
5
+ name = "CompactWorkerError";
6
+ }
7
+ export class CompactWorkerClient {
8
+ options;
9
+ child;
10
+ lines;
11
+ nextId = 1;
12
+ pending = new Map();
13
+ closing = false;
14
+ constructor(options = {}) {
15
+ this.options = {
16
+ ...options,
17
+ command: options.command ?? "afu-context",
18
+ args: options.args ?? ["compact", "worker"],
19
+ timeoutMs: options.timeoutMs ?? 30_000,
20
+ };
21
+ }
22
+ async ping(signal) {
23
+ await this.call("ping", {}, signal);
24
+ }
25
+ async compact(request, signal) {
26
+ const value = await this.call("compact", { request: request }, signal);
27
+ if (!isCompactResult(value)) {
28
+ throw new CompactWorkerError("worker returned an invalid CompactResult");
29
+ }
30
+ return value;
31
+ }
32
+ async drain(signal) {
33
+ await this.call("drain", {}, signal);
34
+ }
35
+ async close(options = {}) {
36
+ if (this.child === undefined)
37
+ return;
38
+ this.closing = true;
39
+ const child = this.child;
40
+ let stopped = false;
41
+ try {
42
+ await this.call("shutdown", { drain: options.drain ?? true }, undefined, true);
43
+ stopped = true;
44
+ }
45
+ finally {
46
+ this.lines?.close();
47
+ child.stdin.end();
48
+ if (!stopped && child.exitCode === null && !child.killed)
49
+ child.kill();
50
+ this.child = undefined;
51
+ this.lines = undefined;
52
+ this.rejectAll(new CompactWorkerError("worker closed"));
53
+ if (process.platform === "win32" && child.pid !== undefined && child.exitCode === null && child.signalCode === null) {
54
+ // Windows keeps the worker's cwd/files locked until the process exits.
55
+ await new Promise((resolve, reject) => {
56
+ const finished = () => { clearTimeout(kill); clearTimeout(deadline); resolve(); };
57
+ const kill = setTimeout(() => child.kill(), 5000);
58
+ const deadline = setTimeout(() => {
59
+ child.off("exit", finished);
60
+ reject(new CompactWorkerError("worker did not exit after shutdown"));
61
+ }, 10000);
62
+ child.once("exit", finished);
63
+ });
64
+ }
65
+ }
66
+ }
67
+ call(method, params, signal, allowWhileClosing = false) {
68
+ if (this.closing && !allowWhileClosing) {
69
+ return Promise.reject(new CompactWorkerError("worker is closing"));
70
+ }
71
+ if (signal?.aborted) {
72
+ return Promise.reject(abortError(signal));
73
+ }
74
+ const child = this.ensureStarted();
75
+ const id = `${process.pid}-${this.nextId++}`;
76
+ return new Promise((resolve, reject) => {
77
+ const pending = { resolve, reject };
78
+ if (this.options.timeoutMs > 0) {
79
+ pending.timer = setTimeout(() => {
80
+ this.pending.delete(id);
81
+ pending.removeAbort?.();
82
+ reject(new CompactWorkerError(`worker request timed out after ${this.options.timeoutMs}ms`));
83
+ }, this.options.timeoutMs);
84
+ }
85
+ if (signal !== undefined) {
86
+ const onAbort = () => {
87
+ this.pending.delete(id);
88
+ if (pending.timer !== undefined)
89
+ clearTimeout(pending.timer);
90
+ reject(abortError(signal));
91
+ };
92
+ signal.addEventListener("abort", onAbort, { once: true });
93
+ pending.removeAbort = () => signal.removeEventListener("abort", onAbort);
94
+ }
95
+ this.pending.set(id, pending);
96
+ const payload = JSON.stringify({ id, method, params });
97
+ child.stdin.write(`${payload}\n`, (error) => {
98
+ if (error === null || error === undefined)
99
+ return;
100
+ this.finishPending(id, undefined, new CompactWorkerError(error.message));
101
+ });
102
+ });
103
+ }
104
+ ensureStarted() {
105
+ if (this.child !== undefined)
106
+ return this.child;
107
+ const child = spawn(this.options.command, this.options.args, {
108
+ cwd: this.options.cwd,
109
+ env: { ...process.env, ...this.options.env },
110
+ stdio: ["pipe", "pipe", "pipe"],
111
+ });
112
+ this.child = child;
113
+ // A worker that dies mid-write emits EPIPE on stdin; without a listener
114
+ // Node treats that as fatal and takes the host process down with it.
115
+ child.stdin.on("error", (error) => {
116
+ this.rejectAll(new CompactWorkerError(`worker input closed: ${error.message}`));
117
+ });
118
+ this.lines = createInterface({ input: child.stdout });
119
+ this.lines.on("line", (line) => this.handleLine(line));
120
+ child.stderr.on("data", (chunk) => {
121
+ const text = chunk.toString("utf8");
122
+ if (this.options.onStderr !== undefined)
123
+ this.options.onStderr(text);
124
+ else
125
+ process.stderr.write(`[afu_context compact worker] ${text}`);
126
+ });
127
+ child.on("error", (error) => {
128
+ // A spawn failure emits "error" without "exit": forget this child so
129
+ // the next call starts a fresh worker instead of writing to a dead pipe.
130
+ if (this.child === child) {
131
+ this.child = undefined;
132
+ this.lines?.close();
133
+ this.lines = undefined;
134
+ }
135
+ this.rejectAll(new CompactWorkerError(`worker failed to start: ${error.message}`));
136
+ });
137
+ child.on("exit", (code, signal) => {
138
+ const expected = this.closing && code === 0;
139
+ if (!expected || this.pending.size > 0) {
140
+ this.rejectAll(new CompactWorkerError(`worker exited (code=${String(code)}, signal=${String(signal)})`));
141
+ }
142
+ if (this.child === child) {
143
+ this.child = undefined;
144
+ this.lines = undefined;
145
+ }
146
+ });
147
+ return child;
148
+ }
149
+ handleLine(line) {
150
+ let value;
151
+ try {
152
+ value = JSON.parse(line);
153
+ }
154
+ catch {
155
+ this.rejectAll(new CompactWorkerError("worker wrote non-JSON data to stdout"));
156
+ return;
157
+ }
158
+ if (!isWorkerResponse(value)) {
159
+ this.rejectAll(new CompactWorkerError("worker wrote an invalid response envelope"));
160
+ return;
161
+ }
162
+ if (value.ok && value.result !== undefined) {
163
+ this.finishPending(value.id, value.result);
164
+ return;
165
+ }
166
+ const message = value.error?.message ?? "worker request failed";
167
+ const type = value.error?.type;
168
+ this.finishPending(value.id, undefined, new CompactWorkerError(type === undefined ? message : `${type}: ${message}`));
169
+ }
170
+ finishPending(id, value, error) {
171
+ const pending = this.pending.get(id);
172
+ if (pending === undefined)
173
+ return;
174
+ this.pending.delete(id);
175
+ if (pending.timer !== undefined)
176
+ clearTimeout(pending.timer);
177
+ pending.removeAbort?.();
178
+ if (error !== undefined)
179
+ pending.reject(error);
180
+ else
181
+ pending.resolve(value ?? null);
182
+ }
183
+ rejectAll(error) {
184
+ for (const id of this.pending.keys())
185
+ this.finishPending(id, undefined, error);
186
+ }
187
+ }
188
+ export function workerOptionsFromEnv(env = process.env) {
189
+ const encodedArgs = env.AFU_CONTEXT_COMPACT_WORKER_ARGS;
190
+ let args;
191
+ if (encodedArgs !== undefined) {
192
+ const decoded = JSON.parse(encodedArgs);
193
+ if (!Array.isArray(decoded) || !decoded.every((value) => typeof value === "string")) {
194
+ throw new CompactWorkerError("AFU_CONTEXT_COMPACT_WORKER_ARGS must be a JSON string array");
195
+ }
196
+ args = decoded;
197
+ }
198
+ const encodedTimeout = env.AFU_CONTEXT_COMPACT_WORKER_TIMEOUT_MS;
199
+ const timeoutMs = encodedTimeout === undefined ? undefined : Number(encodedTimeout);
200
+ if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
201
+ throw new CompactWorkerError("AFU_CONTEXT_COMPACT_WORKER_TIMEOUT_MS must be a non-negative number");
202
+ }
203
+ return {
204
+ ...(env.AFU_CONTEXT_COMPACT_WORKER_COMMAND === undefined ? {} : { command: env.AFU_CONTEXT_COMPACT_WORKER_COMMAND }),
205
+ ...(args === undefined ? {} : { args }),
206
+ ...(env.AFU_CONTEXT_COMPACT_WORKER_CWD === undefined ? {} : { cwd: env.AFU_CONTEXT_COMPACT_WORKER_CWD }),
207
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
208
+ };
209
+ }
210
+ function abortError(signal) {
211
+ return signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
212
+ }
213
+ function isWorkerResponse(value) {
214
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.ok !== "boolean")
215
+ return false;
216
+ if (value.result !== undefined && !isJsonValue(value.result))
217
+ return false;
218
+ return value.error === undefined || isRecord(value.error);
219
+ }
220
+ function isCompactResult(value) {
221
+ return (isRecord(value) &&
222
+ value.schema_version === "1" &&
223
+ Array.isArray(value.items) &&
224
+ Array.isArray(value.logical_items) &&
225
+ isRecord(value.usage) &&
226
+ isRecord(value.async_job));
227
+ }
228
+ function isRecord(value) {
229
+ return typeof value === "object" && value !== null && !Array.isArray(value);
230
+ }
231
+ function isJsonValue(value) {
232
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
233
+ return true;
234
+ }
235
+ if (Array.isArray(value))
236
+ return value.every(isJsonValue);
237
+ return isRecord(value) && Object.values(value).every(isJsonValue);
238
+ }
239
+ export { HostCompactPool, bindingKey } from "./host-worker.js";
240
+ export { journal, sessionHash, afuTrigger } from "./telemetry.js";
241
+ export { summaryPrefix } from "./prefix.js";
242
+ export { afuStatus, afuDashboard, STATUS_COMMAND, DASHBOARD_COMMAND } from "./commands.js";
243
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,12 @@
1
+ /** Keep the host prefix unchanged; coverage instructions are appended at its end. */
2
+ export function summaryPrefix(entries, ids, prompt) {
3
+ if (!ids?.length)
4
+ return { messages: [], prompt }; // artifact-only consolidation
5
+ const start = entries.findIndex(entry => entry.id === ids[0]);
6
+ if (start < 0 || ids.some((id, i) => entries[start + i]?.id !== id))
7
+ throw new Error("Summary source no longer matches host history");
8
+ const before = entries.slice(0, start).flatMap(e => [...e.messages]).length;
9
+ const messages = entries.slice(0, start + ids.length).flatMap(e => [...e.messages]);
10
+ return { messages, prompt: before === 0 ? prompt : `${prompt}\nSource scope: use only messages ${before + 1} through ${messages.length} (one-based) as new source, together with any previous summary supplied in this request. Earlier messages are context only; do not import additional facts from them. Return one replacement summary.` };
11
+ }
12
+ //# sourceMappingURL=prefix.js.map
@@ -0,0 +1,79 @@
1
+ export function findAppliedArtifact(result) {
2
+ for (const item of result.items) {
3
+ const replaced = item.metadata?.replaced_item_ids;
4
+ if (item.kind !== "artifact" || !Array.isArray(replaced))
5
+ continue;
6
+ const replacedItemIds = replaced.filter((value) => typeof value === "string");
7
+ if (replacedItemIds.length === 0)
8
+ continue;
9
+ const artifactId = item.metadata?.artifact_id;
10
+ return {
11
+ item,
12
+ replacedItemIds,
13
+ ...(typeof artifactId === "string" ? { artifactId } : {}),
14
+ };
15
+ }
16
+ return undefined;
17
+ }
18
+ export function normalizeContent(value) {
19
+ if (typeof value === "string")
20
+ return value;
21
+ if (Array.isArray(value) && value.every(isJsonObject)) {
22
+ return structuredClone(value);
23
+ }
24
+ return safeStringify(value);
25
+ }
26
+ /** Same weights as the Python HeuristicTokenEstimator. */
27
+ function characterWeight(code) {
28
+ if ((code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3400 && code <= 0x4dbf))
29
+ return 1;
30
+ if ((code >= 0xac00 && code <= 0xd7af) || (code >= 0x1100 && code <= 0x11ff))
31
+ return 1;
32
+ if (code >= 0x3040 && code <= 0x30ff)
33
+ return 1;
34
+ if (code >= 0x1f000)
35
+ return 1;
36
+ if (code >= 0x0370)
37
+ return 0.5;
38
+ return 0.25;
39
+ }
40
+ export function estimateContentTokens(content) {
41
+ const text = typeof content === "string" ? content : JSON.stringify(content);
42
+ let units = 0;
43
+ for (const character of text)
44
+ units += characterWeight(character.codePointAt(0) ?? 0);
45
+ return text.length === 0 ? 0 : Math.max(1, Math.ceil(units));
46
+ }
47
+ function isJsonObject(value) {
48
+ if (typeof value !== "object" || value === null || Array.isArray(value))
49
+ return false;
50
+ return Object.values(value).every(isJsonValue);
51
+ }
52
+ function isJsonValue(value) {
53
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
54
+ return true;
55
+ }
56
+ if (Array.isArray(value))
57
+ return value.every(isJsonValue);
58
+ return isJsonObject(value);
59
+ }
60
+ function safeStringify(value) {
61
+ try {
62
+ const encoded = JSON.stringify(value);
63
+ return encoded ?? String(value);
64
+ }
65
+ catch {
66
+ return String(value);
67
+ }
68
+ }
69
+ /** An opaque observation ID travels with the exact replacement artifact. */
70
+ export function observationId(result) {
71
+ const applied = findAppliedArtifact(result);
72
+ if (!applied?.artifactId)
73
+ return undefined;
74
+ const artifact = result.artifacts.find(a => a.id === applied.artifactId);
75
+ const metadata = artifact?.metadata;
76
+ const id = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata.observation_id : undefined;
77
+ return typeof id === "string" && /^[a-f0-9]{24}$/.test(id) ? id : undefined;
78
+ }
79
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1,78 @@
1
+ import { appendFileSync, openSync, closeSync, mkdirSync, statSync, renameSync, constants, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { randomUUID, createHash } from 'node:crypto';
5
+ export const sessionHash = (value) => createHash('sha256').update(value).digest('hex').slice(0, 16);
6
+ /**
7
+ * Context window and the token count at which AFU starts a background summary,
8
+ * matching the worker (afu_context.local_worker): an explicit window override,
9
+ * then AFU_TRIGGER_RATIO, else 0.5 for windows of 1M tokens or more and 0.6 below.
10
+ */
11
+ export function afuTrigger(hostWindow, env = process.env, _platform = process.platform) {
12
+ const override = env.AFU_HOST_CONTEXT_WINDOW_OVERRIDE;
13
+ const window = override && override !== 'auto' ? Number(override) : hostWindow;
14
+ if (!Number.isSafeInteger(window) || !window || window < 1)
15
+ return {};
16
+ const configured = env.AFU_TRIGGER_RATIO && env.AFU_TRIGGER_RATIO !== 'auto' ? Number(env.AFU_TRIGGER_RATIO) : NaN;
17
+ const fallback = window >= 1_000_000 ? 0.5 : 0.6;
18
+ const ratio = configured > 0 && configured < 1 ? configured : fallback;
19
+ return { context_window: window, threshold: Math.floor(window * ratio) };
20
+ }
21
+ const journals = new Map();
22
+ export function journal(host) {
23
+ let log = journals.get(host);
24
+ if (!log) {
25
+ const created = createJournal(host);
26
+ journals.set(host, created);
27
+ created('runtime_started');
28
+ // One runtime_stopped per process, however many times the host recycles
29
+ // the adapter (DSH disposes an engine per agent scope).
30
+ process.once('exit', () => created('runtime_stopped'));
31
+ log = created;
32
+ }
33
+ return (event, fields) => { if (event !== 'runtime_stopped')
34
+ log(event, fields); };
35
+ }
36
+ function createJournal(host) {
37
+ const root = join(process.env.AFU_STATE_DIR ?? join(homedir(), '.local/share/afu-context'), 'logs');
38
+ const id = randomUUID().replaceAll('-', '');
39
+ const file = join(root, `events-${process.pid}-${id}.jsonl`);
40
+ let version = '0.1.0', build = 'development';
41
+ try {
42
+ const m = JSON.parse(readFileSync(join(process.env.AFU_INSTALL_ROOT ?? process.cwd(), 'release.json'), 'utf8'));
43
+ if (/^[a-f0-9]{12,64}$/.test(m.build))
44
+ build = m.build;
45
+ if (/^[0-9.]+$/.test(m.version))
46
+ version = m.version;
47
+ }
48
+ catch { }
49
+ return (event, fields = {}) => {
50
+ try {
51
+ const row = { event, host, version, build, runtime_id: id, pid: process.pid, time: new Date().toISOString() };
52
+ // COSEC: diagnostics never include raw model IDs, credentials, prompts or errors.
53
+ for (const [k, v] of Object.entries(fields)) {
54
+ if (['session', 'artifact_id', 'phase', 'reason'].includes(k) && typeof v === 'string' && /^[A-Za-z0-9_.:+-]{1,100}$/.test(v))
55
+ row[k] = v;
56
+ if (['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens', 'context_window', 'threshold', 'estimated_input_tokens', 'removed_tokens_estimate', 'duration_ms', 'host_compaction_ms'].includes(k) && typeof v === 'number' && Number.isFinite(v) && v >= 0)
57
+ row[k] = v;
58
+ if (['cache_usage_known', 'applied', 'upstream_success'].includes(k) && typeof v === 'boolean')
59
+ row[k] = v;
60
+ }
61
+ mkdirSync(root, { recursive: true, mode: 0o700 });
62
+ try {
63
+ if (statSync(file).size > 2 * 1024 * 1024)
64
+ renameSync(file, file.replace('.jsonl', '.previous.jsonl'));
65
+ }
66
+ catch { }
67
+ const fd = openSync(file, constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | (constants.O_NOFOLLOW ?? 0), 0o600);
68
+ try {
69
+ appendFileSync(fd, JSON.stringify(row) + '\n');
70
+ }
71
+ finally {
72
+ closeSync(fd);
73
+ }
74
+ }
75
+ catch { } // Logging must never break a host request.
76
+ };
77
+ }
78
+ //# sourceMappingURL=telemetry.js.map