@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.
- package/LICENSE +16 -0
- package/README.md +10 -0
- package/dist/compaction.d.ts +69 -0
- package/dist/compaction.js +174 -0
- package/dist/delegate.d.ts +84 -0
- package/dist/delegate.js +135 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/mcp.d.ts +13 -0
- package/dist/mcp.js +78 -0
- package/dist/memory.d.ts +7 -0
- package/dist/memory.js +33 -0
- package/dist/model/openrouter.d.ts +61 -0
- package/dist/model/openrouter.js +135 -0
- package/dist/model/router.d.ts +60 -0
- package/dist/model/router.js +171 -0
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.js +16 -0
- package/dist/prompt.d.ts +5 -0
- package/dist/prompt.js +31 -0
- package/dist/scripts/compaction-live.d.ts +1 -0
- package/dist/scripts/compaction-live.js +80 -0
- package/dist/scripts/compaction-smoke.d.ts +1 -0
- package/dist/scripts/compaction-smoke.js +143 -0
- package/dist/scripts/delegation-live.d.ts +1 -0
- package/dist/scripts/delegation-live.js +122 -0
- package/dist/scripts/delegation-smoke.d.ts +1 -0
- package/dist/scripts/delegation-smoke.js +140 -0
- package/dist/scripts/router-live.d.ts +1 -0
- package/dist/scripts/router-live.js +73 -0
- package/dist/scripts/router-smoke.d.ts +1 -0
- package/dist/scripts/router-smoke.js +58 -0
- package/dist/scripts/smoke.d.ts +1 -0
- package/dist/scripts/smoke.js +52 -0
- package/dist/session.d.ts +73 -0
- package/dist/session.js +574 -0
- package/dist/skills.d.ts +14 -0
- package/dist/skills.js +56 -0
- package/dist/tools/bash.d.ts +2 -0
- package/dist/tools/bash.js +38 -0
- package/dist/tools/fs-tools.d.ts +5 -0
- package/dist/tools/fs-tools.js +115 -0
- package/dist/tools/registry.d.ts +5 -0
- package/dist/tools/registry.js +12 -0
- package/dist/tools/search-tools.d.ts +3 -0
- package/dist/tools/search-tools.js +84 -0
- package/dist/tools/types.d.ts +27 -0
- package/dist/tools/types.js +15 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.js +2 -0
- package/dist/vault.d.ts +13 -0
- package/dist/vault.js +81 -0
- 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
|
package/dist/prompt.d.ts
ADDED
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 {};
|