@9thprotocol/agent-core 0.1.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.
Files changed (53) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +10 -0
  3. package/dist/compaction.d.ts +69 -0
  4. package/dist/compaction.js +174 -0
  5. package/dist/delegate.d.ts +84 -0
  6. package/dist/delegate.js +135 -0
  7. package/dist/index.d.ts +18 -0
  8. package/dist/index.js +18 -0
  9. package/dist/mcp.d.ts +13 -0
  10. package/dist/mcp.js +78 -0
  11. package/dist/memory.d.ts +7 -0
  12. package/dist/memory.js +33 -0
  13. package/dist/model/openrouter.d.ts +61 -0
  14. package/dist/model/openrouter.js +135 -0
  15. package/dist/model/router.d.ts +60 -0
  16. package/dist/model/router.js +171 -0
  17. package/dist/permissions.d.ts +5 -0
  18. package/dist/permissions.js +16 -0
  19. package/dist/prompt.d.ts +5 -0
  20. package/dist/prompt.js +31 -0
  21. package/dist/scripts/compaction-live.d.ts +1 -0
  22. package/dist/scripts/compaction-live.js +80 -0
  23. package/dist/scripts/compaction-smoke.d.ts +1 -0
  24. package/dist/scripts/compaction-smoke.js +143 -0
  25. package/dist/scripts/delegation-live.d.ts +1 -0
  26. package/dist/scripts/delegation-live.js +122 -0
  27. package/dist/scripts/delegation-smoke.d.ts +1 -0
  28. package/dist/scripts/delegation-smoke.js +140 -0
  29. package/dist/scripts/router-live.d.ts +1 -0
  30. package/dist/scripts/router-live.js +73 -0
  31. package/dist/scripts/router-smoke.d.ts +1 -0
  32. package/dist/scripts/router-smoke.js +58 -0
  33. package/dist/scripts/smoke.d.ts +1 -0
  34. package/dist/scripts/smoke.js +52 -0
  35. package/dist/session.d.ts +73 -0
  36. package/dist/session.js +574 -0
  37. package/dist/skills.d.ts +14 -0
  38. package/dist/skills.js +56 -0
  39. package/dist/tools/bash.d.ts +2 -0
  40. package/dist/tools/bash.js +38 -0
  41. package/dist/tools/fs-tools.d.ts +5 -0
  42. package/dist/tools/fs-tools.js +115 -0
  43. package/dist/tools/registry.d.ts +5 -0
  44. package/dist/tools/registry.js +12 -0
  45. package/dist/tools/search-tools.d.ts +3 -0
  46. package/dist/tools/search-tools.js +84 -0
  47. package/dist/tools/types.d.ts +27 -0
  48. package/dist/tools/types.js +15 -0
  49. package/dist/types.d.ts +130 -0
  50. package/dist/types.js +2 -0
  51. package/dist/vault.d.ts +13 -0
  52. package/dist/vault.js +81 -0
  53. package/package.json +29 -0
@@ -0,0 +1,61 @@
1
+ import type { UsageTotals } from "../types.js";
2
+ /** Non-2xx from OpenRouter or the platform proxy, with the platform's error code when present. */
3
+ export declare class ApiError extends Error {
4
+ readonly status: number;
5
+ readonly code?: string | undefined;
6
+ constructor(status: number, message: string, code?: string | undefined);
7
+ }
8
+ export interface RawToolCall {
9
+ id: string;
10
+ type: "function";
11
+ function: {
12
+ name: string;
13
+ arguments: string;
14
+ };
15
+ }
16
+ export type ChatMessage = {
17
+ role: "system" | "user";
18
+ content: string;
19
+ } | {
20
+ role: "assistant";
21
+ content: string | null;
22
+ tool_calls?: RawToolCall[];
23
+ } | {
24
+ role: "tool";
25
+ tool_call_id: string;
26
+ content: string;
27
+ };
28
+ export interface ToolSchema {
29
+ type: "function";
30
+ function: {
31
+ name: string;
32
+ description: string;
33
+ parameters: Record<string, unknown>;
34
+ };
35
+ }
36
+ export interface AssistantResult {
37
+ message: Extract<ChatMessage, {
38
+ role: "assistant";
39
+ }>;
40
+ usage: UsageTotals;
41
+ finishReason: string | null;
42
+ }
43
+ export type StreamEvent = {
44
+ type: "delta";
45
+ text: string;
46
+ } | {
47
+ type: "done";
48
+ result: AssistantResult;
49
+ };
50
+ /** One streaming chat completion with tool support. Yields text deltas, then `done`. */
51
+ export declare function streamChat(opts: {
52
+ apiKey: string;
53
+ model: string;
54
+ messages: ChatMessage[];
55
+ tools: ToolSchema[];
56
+ signal?: AbortSignal;
57
+ /** Override the OpenRouter base URL (e.g. the 9th Protocol metering proxy). */
58
+ baseUrl?: string;
59
+ /** Extra top-level body fields (e.g. platform `context`). */
60
+ extraBody?: Record<string, unknown>;
61
+ }): AsyncGenerator<StreamEvent>;
@@ -0,0 +1,135 @@
1
+ const BASE = "https://openrouter.ai/api/v1";
2
+ /** Non-2xx from OpenRouter or the platform proxy, with the platform's error code when present. */
3
+ export class ApiError extends Error {
4
+ status;
5
+ code;
6
+ constructor(status, message, code) {
7
+ super(message);
8
+ this.status = status;
9
+ this.code = code;
10
+ }
11
+ }
12
+ /**
13
+ * Anthropic models honor prompt caching via cache_control breakpoints; marking the
14
+ * system prompt caches the stable prefix (cache reads are 5-10x cheaper than input).
15
+ */
16
+ function withCaching(model, messages) {
17
+ if (!model.startsWith("anthropic/"))
18
+ return messages;
19
+ return messages.map((m) => m.role === "system"
20
+ ? {
21
+ role: "system",
22
+ content: [
23
+ { type: "text", text: m.content, cache_control: { type: "ephemeral" } },
24
+ ],
25
+ }
26
+ : m);
27
+ }
28
+ /** One streaming chat completion with tool support. Yields text deltas, then `done`. */
29
+ export async function* streamChat(opts) {
30
+ const res = await fetch(`${opts.baseUrl ?? BASE}/chat/completions`, {
31
+ method: "POST",
32
+ headers: {
33
+ Authorization: `Bearer ${opts.apiKey}`,
34
+ "Content-Type": "application/json",
35
+ "X-Title": "9th Protocol",
36
+ },
37
+ signal: opts.signal,
38
+ body: JSON.stringify({
39
+ model: opts.model,
40
+ messages: withCaching(opts.model, opts.messages),
41
+ tools: opts.tools,
42
+ stream: true,
43
+ usage: { include: true },
44
+ ...opts.extraBody,
45
+ }),
46
+ });
47
+ if (!res.ok || !res.body) {
48
+ const text = await res.text();
49
+ let code;
50
+ let message = text;
51
+ try {
52
+ const parsed = JSON.parse(text);
53
+ if (typeof parsed.error === "string")
54
+ code = parsed.error;
55
+ if (typeof parsed.message === "string")
56
+ message = parsed.message;
57
+ }
58
+ catch {
59
+ // non-JSON error body
60
+ }
61
+ throw new ApiError(res.status, `${res.status}: ${message}`, code);
62
+ }
63
+ let text = "";
64
+ let finishReason = null;
65
+ const usage = { inputTokens: 0, cachedTokens: 0, outputTokens: 0, requests: 1 };
66
+ const calls = [];
67
+ const decoder = new TextDecoder();
68
+ let buffer = "";
69
+ const reader = res.body.getReader();
70
+ while (true) {
71
+ const { done, value } = await reader.read();
72
+ if (done)
73
+ break;
74
+ buffer += decoder.decode(value, { stream: true });
75
+ let nl;
76
+ while ((nl = buffer.indexOf("\n")) !== -1) {
77
+ const line = buffer.slice(0, nl).trim();
78
+ buffer = buffer.slice(nl + 1);
79
+ if (!line.startsWith("data: "))
80
+ continue; // ignore comments/keepalives
81
+ const payload = line.slice(6);
82
+ if (payload === "[DONE]")
83
+ continue;
84
+ let chunk;
85
+ try {
86
+ chunk = JSON.parse(payload);
87
+ }
88
+ catch {
89
+ continue; // partial/garbled line, skip
90
+ }
91
+ const choice = chunk.choices?.[0];
92
+ if (choice?.delta?.content) {
93
+ text += choice.delta.content;
94
+ yield { type: "delta", text: choice.delta.content };
95
+ }
96
+ for (const tc of choice?.delta?.tool_calls ?? []) {
97
+ const i = tc.index ?? 0;
98
+ let slot = calls[i];
99
+ if (!slot) {
100
+ slot = { id: "", name: "", args: "" };
101
+ calls[i] = slot;
102
+ }
103
+ if (tc.id)
104
+ slot.id = tc.id;
105
+ if (tc.function?.name)
106
+ slot.name = tc.function.name;
107
+ if (tc.function?.arguments)
108
+ slot.args += tc.function.arguments;
109
+ }
110
+ if (choice?.finish_reason)
111
+ finishReason = choice.finish_reason;
112
+ if (chunk.usage) {
113
+ usage.inputTokens = chunk.usage.prompt_tokens ?? 0;
114
+ usage.outputTokens = chunk.usage.completion_tokens ?? 0;
115
+ usage.cachedTokens = chunk.usage.prompt_tokens_details?.cached_tokens ?? 0;
116
+ }
117
+ }
118
+ }
119
+ const tool_calls = calls
120
+ .filter((c) => Boolean(c && c.id && c.name))
121
+ .map((c) => ({ id: c.id, type: "function", function: { name: c.name, arguments: c.args } }));
122
+ yield {
123
+ type: "done",
124
+ result: {
125
+ message: {
126
+ role: "assistant",
127
+ content: text || null,
128
+ ...(tool_calls.length ? { tool_calls } : {}),
129
+ },
130
+ usage,
131
+ finishReason,
132
+ },
133
+ };
134
+ }
135
+ //# sourceMappingURL=openrouter.js.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `Auto` model routing, PLAN.md §5.5.
3
+ *
4
+ * v1 is pure heuristics, deliberately: the spec allows either a cheap classifier
5
+ * model or heuristics, and heuristics cost no extra call, add no latency, and
6
+ * cannot themselves fail. The classifier upgrade slots in behind `route()`
7
+ * without touching callers.
8
+ *
9
+ * The router only ever *proposes*; the API still enforces plan gating, so a
10
+ * locked pick would be rejected server-side. Hence `locked` candidates are
11
+ * filtered out before ranking rather than trusted to fail gracefully.
12
+ */
13
+ export declare const AUTO_MODEL = "auto";
14
+ export type RouterBias = "economy" | "balanced" | "quality";
15
+ export type TaskComplexity = "trivial" | "normal" | "complex";
16
+ export type ModelTier = "economy" | "standard" | "premium";
17
+ export interface RouterCandidate {
18
+ id: string;
19
+ tier: ModelTier;
20
+ /** True when the user's plan cannot use this model (from GET /v1/models). */
21
+ locked?: boolean;
22
+ /** Context window, used to decide when to compact. */
23
+ contextLength?: number | null;
24
+ }
25
+ export interface RouteInput {
26
+ /** The user's message for this turn. */
27
+ text: string;
28
+ bias?: RouterBias;
29
+ /** Plan mode means "think, don't edit". Always treated as complex work. */
30
+ planMode?: boolean;
31
+ /** Explore sub-agents are read-only searchers; they never need a strong model. */
32
+ subagent?: "explore" | "general";
33
+ /** Catalog with per-plan locks. Empty = BYOK, where every id is fair game. */
34
+ available?: RouterCandidate[];
35
+ }
36
+ export interface RouteDecision {
37
+ model: string;
38
+ complexity: TaskComplexity;
39
+ reason: string;
40
+ }
41
+ /** What hosts show for an Auto session that has not routed a turn yet. */
42
+ export declare const FALLBACK_DISPLAY_MODEL = "moonshotai/kimi-k2.7-code";
43
+ /** Classify how much model the turn deserves. Exported for tests and debugging. */
44
+ export declare function classify(text: string, planMode?: boolean): TaskComplexity;
45
+ /**
46
+ * Choose a model for one turn.
47
+ *
48
+ * Order of precedence: explore sub-agents are pinned to economy, then bias
49
+ * shifts the classified complexity, then the ladder is filtered by plan locks.
50
+ */
51
+ export declare function route(input: RouteInput): RouteDecision;
52
+ /**
53
+ * Model for delegated I/O work (see `delegate.ts`).
54
+ *
55
+ * Reuses the trivial ladder rather than naming a model of its own, so the
56
+ * cheapest usable model stays defined in exactly one place and plan locks are
57
+ * honoured for free. Bulk reading and boilerplate are the definition of a
58
+ * trivial turn, which is why the ladder is already correct for it.
59
+ */
60
+ export declare function workerModel(available?: RouterCandidate[]): string;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * `Auto` model routing, PLAN.md §5.5.
3
+ *
4
+ * v1 is pure heuristics, deliberately: the spec allows either a cheap classifier
5
+ * model or heuristics, and heuristics cost no extra call, add no latency, and
6
+ * cannot themselves fail. The classifier upgrade slots in behind `route()`
7
+ * without touching callers.
8
+ *
9
+ * The router only ever *proposes*; the API still enforces plan gating, so a
10
+ * locked pick would be rejected server-side. Hence `locked` candidates are
11
+ * filtered out before ranking rather than trusted to fail gracefully.
12
+ */
13
+ export const AUTO_MODEL = "auto";
14
+ /**
15
+ * Preference ladders per complexity, best-first, from PLAN.md §5.5's routing
16
+ * table. Ids must exist in the catalog (api/src/constants/models.ts).
17
+ */
18
+ const LADDERS = {
19
+ trivial: [
20
+ "z-ai/glm-4.7-flash",
21
+ "qwen/qwen3-coder-flash",
22
+ "google/gemini-2.5-flash",
23
+ "z-ai/glm-4.7",
24
+ "moonshotai/kimi-k2.7-code",
25
+ ],
26
+ normal: [
27
+ "moonshotai/kimi-k2.7-code",
28
+ "z-ai/glm-4.7",
29
+ "qwen/qwen3-coder",
30
+ "deepseek/deepseek-v3.2",
31
+ "minimax/minimax-m2.5",
32
+ ],
33
+ // Sonnet 5 leads on cost-per-quality: ~$11.80 for a heavy session against
34
+ // ~$17.70 for Kimi K3, for at least comparable coding output. K3 is
35
+ // deliberately absent from every Auto ladder. It is the one model whose price
36
+ // surprises people (frontier-priced despite being open), so Auto never spends
37
+ // a user's window on it. Max users can still pick it explicitly via /model.
38
+ complex: [
39
+ "anthropic/claude-sonnet-5",
40
+ "openai/gpt-5.3-codex",
41
+ "google/gemini-2.5-pro",
42
+ "z-ai/glm-5.2",
43
+ "moonshotai/kimi-k2.7-code",
44
+ ],
45
+ };
46
+ /** Only reached on `quality` bias for genuinely hard turns, Max-plan territory. */
47
+ const PREMIUM_LADDER = [
48
+ "anthropic/claude-fable-5",
49
+ "anthropic/claude-opus-4.8",
50
+ "anthropic/claude-opus-4.8-fast",
51
+ ];
52
+ /** Last-resort pick when nothing in a ladder is available (also the BYOK anchor). */
53
+ const FALLBACK_MODEL = "moonshotai/kimi-k2.7-code";
54
+ /** What hosts show for an Auto session that has not routed a turn yet. */
55
+ export const FALLBACK_DISPLAY_MODEL = FALLBACK_MODEL;
56
+ const COMPLEX_PATTERNS = [
57
+ /\brefactor/i,
58
+ /\barchitect/i,
59
+ /\bdesign\b/i,
60
+ /\bdebug/i,
61
+ /\bmigrat/i,
62
+ /\binvestigat/i,
63
+ /\brewrite/i,
64
+ /\broot cause/i,
65
+ /\brace condition/i,
66
+ /\bmemory leak/i,
67
+ /\bperformance\b/i,
68
+ /\bsecurity\b/i,
69
+ /\bwhy (is|does|are|did)\b/i,
70
+ /\bmulti[- ]file/i,
71
+ /\bacross (the )?(codebase|repo|project)/i,
72
+ /\bplan\b/i,
73
+ ];
74
+ const TRIVIAL_PATTERNS = [
75
+ /\brename\b/i,
76
+ /\btypo/i,
77
+ /\badd a comment/i,
78
+ /\bformat\b/i,
79
+ /\bbump (the )?version/i,
80
+ /\bwhat (is|does|are)\b/i,
81
+ /\bone[- ]liner/i,
82
+ /\blint\b/i,
83
+ ];
84
+ const LONG_PROMPT_CHARS = 600;
85
+ /**
86
+ * Deliberately tight. Real feature asks are short ("add a logout button to the
87
+ * settings page") and must not fall to a flash model on length alone, genuinely
88
+ * trivial work is caught by TRIVIAL_PATTERNS instead. This only catches terse
89
+ * imperatives like "run the tests".
90
+ */
91
+ const SHORT_PROMPT_CHARS = 30;
92
+ /** Classify how much model the turn deserves. Exported for tests and debugging. */
93
+ export function classify(text, planMode = false) {
94
+ if (planMode)
95
+ return "complex";
96
+ const trimmed = text.trim();
97
+ const complexHits = COMPLEX_PATTERNS.filter((p) => p.test(trimmed)).length;
98
+ const trivialHits = TRIVIAL_PATTERNS.filter((p) => p.test(trimmed)).length;
99
+ // An explicit complex signal outranks brevity: "debug this" is three words.
100
+ if (complexHits > 0)
101
+ return "complex";
102
+ if (trimmed.length >= LONG_PROMPT_CHARS)
103
+ return "complex";
104
+ if (trivialHits > 0 && trimmed.length < LONG_PROMPT_CHARS)
105
+ return "trivial";
106
+ if (trimmed.length <= SHORT_PROMPT_CHARS)
107
+ return "trivial";
108
+ return "normal";
109
+ }
110
+ const ORDER = ["trivial", "normal", "complex"];
111
+ function shift(complexity, steps) {
112
+ const i = ORDER.indexOf(complexity) + steps;
113
+ return ORDER[Math.max(0, Math.min(ORDER.length - 1, i))];
114
+ }
115
+ /** First ladder entry the user can actually use. */
116
+ function pick(ladder, available) {
117
+ if (!available.length)
118
+ return ladder[0] ?? null; // BYOK: no lock data, take the best
119
+ const usable = new Set(available.filter((m) => !m.locked).map((m) => m.id));
120
+ return ladder.find((id) => usable.has(id)) ?? null;
121
+ }
122
+ /**
123
+ * Choose a model for one turn.
124
+ *
125
+ * Order of precedence: explore sub-agents are pinned to economy, then bias
126
+ * shifts the classified complexity, then the ladder is filtered by plan locks.
127
+ */
128
+ export function route(input) {
129
+ const available = input.available ?? [];
130
+ const bias = input.bias ?? "balanced";
131
+ // Explore sub-agents read and summarise; a frontier model here is pure burn.
132
+ if (input.subagent === "explore") {
133
+ return {
134
+ model: pick(LADDERS.trivial, available) ?? FALLBACK_MODEL,
135
+ complexity: "trivial",
136
+ reason: "explore sub-agent. Read-only search, economy tier",
137
+ };
138
+ }
139
+ const base = classify(input.text, input.planMode);
140
+ const adjusted = bias === "economy" ? shift(base, -1) : bias === "quality" ? shift(base, 1) : base;
141
+ // Quality bias on genuinely hard turns is the only path to premium models.
142
+ if (bias === "quality" && base === "complex") {
143
+ const premium = pick(PREMIUM_LADDER, available);
144
+ if (premium) {
145
+ return {
146
+ model: premium,
147
+ complexity: "complex",
148
+ reason: "complex turn, quality bias, premium tier",
149
+ };
150
+ }
151
+ }
152
+ const model = pick(LADDERS[adjusted], available) ?? FALLBACK_MODEL;
153
+ const biasNote = bias === "balanced" ? "" : `, ${bias} bias`;
154
+ return {
155
+ model,
156
+ complexity: adjusted,
157
+ reason: `${base} task${biasNote}${input.planMode ? " (plan mode)" : ""}`,
158
+ };
159
+ }
160
+ /**
161
+ * Model for delegated I/O work (see `delegate.ts`).
162
+ *
163
+ * Reuses the trivial ladder rather than naming a model of its own, so the
164
+ * cheapest usable model stays defined in exactly one place and plan locks are
165
+ * honoured for free. Bulk reading and boilerplate are the definition of a
166
+ * trivial turn, which is why the ladder is already correct for it.
167
+ */
168
+ export function workerModel(available) {
169
+ return pick(LADDERS.trivial, available ?? []) ?? FALLBACK_MODEL;
170
+ }
171
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1,5 @@
1
+ import type { PermissionMode } from "./types.js";
2
+ import type { ToolKind } from "./tools/types.js";
3
+ export type Gate = "allow" | "ask" | "deny";
4
+ /** Which gate a tool kind hits under each permission mode. */
5
+ export declare function gateFor(kind: ToolKind, mode: PermissionMode): Gate;
@@ -0,0 +1,16 @@
1
+ /** Which gate a tool kind hits under each permission mode. */
2
+ export function gateFor(kind, mode) {
3
+ if (kind === "read")
4
+ return "allow";
5
+ switch (mode) {
6
+ case "bypass":
7
+ return "allow";
8
+ case "plan":
9
+ return "deny"; // plan mode is read-only
10
+ case "accept-edits":
11
+ return kind === "mutate" ? "allow" : "ask";
12
+ case "default":
13
+ return "ask";
14
+ }
15
+ }
16
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * System prompt, written from scratch in our own words (PLAN.md §1.3).
3
+ * In Milestone 2 this moves server-side (assembled per request by the API).
4
+ */
5
+ export declare function buildSystemPrompt(cwd: string, memory?: string): string;
package/dist/prompt.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * System prompt, written from scratch in our own words (PLAN.md §1.3).
3
+ * In Milestone 2 this moves server-side (assembled per request by the API).
4
+ */
5
+ export function buildSystemPrompt(cwd, memory) {
6
+ const memoryBlock = memory
7
+ ? `\n\nStanding instructions from the user's 9P.md memory files, follow them:\n${memory}`
8
+ : "";
9
+ return `You are 9p, the 9th Protocol coding agent. You help engineers build, debug, and modify real codebases directly from their terminal.
10
+
11
+ Working directory: ${cwd}
12
+ Platform: ${process.platform}
13
+ Today's date: ${new Date().toISOString().slice(0, 10)}
14
+
15
+ How you work:
16
+ - Investigate before acting. Locate relevant files with glob/grep, read them, and only then edit. Never guess at file contents or APIs, look them up.
17
+ - Prefer small, targeted edits over rewriting files. Match the surrounding code's style, naming, and conventions; add comments only where intent isn't obvious from the code.
18
+ - Verify your changes when possible: run the project's tests, typechecker, linter, or a quick command. Report results honestly, if something fails, show the failure instead of claiming success.
19
+ - When a command errors, read the error and adapt. Don't repeat an identical failing call.
20
+ - Finish the job. If a step is missing information, gather it with your tools rather than asking when you can find out yourself.
21
+
22
+ Constraints:
23
+ - Stay inside the working directory unless the user points you elsewhere.
24
+ - Destructive operations (rm -rf, git reset --hard, force pushes, dropping data) are off-limits unless the user explicitly asks for them.
25
+ - Never invent tool output, file contents, or command results.
26
+
27
+ Style:
28
+ - Your prose is rendered in a terminal: plain text, short paragraphs, no heavy markdown.
29
+ - Lead with the outcome, then the essentials. Skip filler and restating the question.${memoryBlock}`;
30
+ }
31
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Live compaction check: force a real session past the threshold, then confirm
3
+ * the provider ACCEPTS the compacted history and the agent keeps working.
4
+ *
5
+ * The offline checks prove the message list stays well-formed. Only this proves
6
+ * the provider agrees.
7
+ *
8
+ * PLATFORM_URL=... PLATFORM_TOKEN=... node dist/scripts/compaction-live.js
9
+ */
10
+ import { AgentSession } from "../session.js";
11
+ const platformUrl = process.env.PLATFORM_URL;
12
+ const token = process.env.PLATFORM_TOKEN;
13
+ if (!platformUrl || !token) {
14
+ console.error("Set PLATFORM_URL + PLATFORM_TOKEN");
15
+ process.exit(1);
16
+ }
17
+ const session = new AgentSession({
18
+ apiKey: token,
19
+ model: "z-ai/glm-4.7-flash",
20
+ cwd: process.cwd(),
21
+ mode: "bypass",
22
+ platform: { baseUrl: platformUrl },
23
+ // Tiny window so a handful of turns trips the threshold. Run this in a
24
+ // directory with real files, or the history never grows enough to compact.
25
+ contextTokens: Number(process.env.COMPACT_CONTEXT_TOKENS ?? 1500),
26
+ maxTurnsPerMessage: 6,
27
+ });
28
+ // Reading real files is what makes history genuinely large, which is the
29
+ // situation compaction exists for. A window this size then forces a compaction
30
+ // where the summary is much smaller than what it replaces.
31
+ const prompts = [
32
+ "Read session.ts and say DONE1.",
33
+ "Read compaction.ts and say DONE2.",
34
+ "Read types.ts and say DONE3.",
35
+ "What was the FIRST file I asked you to read in this conversation? Answer with just the filename.",
36
+ ];
37
+ let compactions = 0;
38
+ let shrank = false;
39
+ let lastText = "";
40
+ let failed = false;
41
+ for (const [i, prompt] of prompts.entries()) {
42
+ lastText = "";
43
+ let error = "";
44
+ for await (const ev of session.send(prompt)) {
45
+ if (ev.type === "compacted") {
46
+ compactions++;
47
+ if (ev.afterTokens < ev.beforeTokens)
48
+ shrank = true;
49
+ const pct = Math.round((1 - ev.afterTokens / ev.beforeTokens) * 100);
50
+ console.log(`⊙ compacted ~${ev.beforeTokens} → ~${ev.afterTokens} tokens (${pct}% smaller)`);
51
+ }
52
+ if (ev.type === "text_delta")
53
+ lastText += ev.text;
54
+ if (ev.type === "error")
55
+ error = ev.message;
56
+ }
57
+ if (error) {
58
+ console.log(`✗ turn ${i + 1} errored: ${error.slice(0, 160)}`);
59
+ failed = true;
60
+ break;
61
+ }
62
+ console.log(`✓ turn ${i + 1} ok (~${session.contextTokens} tokens), ${lastText.trim().slice(0, 60).replace(/\n/g, " ")}`);
63
+ }
64
+ console.log(`\ncompactions: ${compactions}`);
65
+ if (!failed && compactions === 0) {
66
+ console.log("✗ never compacted. The threshold did not trip, nothing was proven");
67
+ failed = true;
68
+ }
69
+ if (!failed && !shrank) {
70
+ console.log("✗ compaction ran but never reduced the context");
71
+ failed = true;
72
+ }
73
+ // The last prompt asks about the earliest exchange, which by then exists only
74
+ // inside the summary. A sensible answer means compaction preserved meaning.
75
+ if (!failed) {
76
+ console.log(`\nrecall-after-compaction answer:\n ${lastText.trim().slice(0, 300)}`);
77
+ }
78
+ console.log(`\n${failed ? "FAILED" : "live compaction OK"}`);
79
+ process.exit(failed ? 1 : 0);
80
+ //# sourceMappingURL=compaction-live.js.map
@@ -0,0 +1 @@
1
+ export {};