@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
package/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ 9th Protocol — Proprietary License
2
+
3
+ Copyright (c) 2026 9th Protocol. All rights reserved.
4
+
5
+ This software and its source code are the property of 9th Protocol.
6
+ It is licensed, not sold. You may install and use the unmodified
7
+ package for its intended purpose. You may not copy, modify, merge,
8
+ publish, distribute, sublicense, decompile, or reverse engineer it,
9
+ or use it to build a competing agent product, without a separate
10
+ written license from 9th Protocol.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13
+ EXPRESS OR IMPLIED. IN NO EVENT SHALL 9TH PROTOCOL BE LIABLE FOR
14
+ ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE SOFTWARE.
15
+
16
+ Contact: licensing@9thprotocol.com
package/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # @9thprotocol/agent-core
2
+
3
+ The 9th Protocol agent engine: agent loop, tools, permissions, sub-agents,
4
+ context management, model routing. Proprietary — see [LICENSE](./LICENSE).
5
+
6
+ This is the engine behind [`9p`](https://www.npmjs.com/package/@9thprotocol/cli),
7
+ the 9th Protocol terminal coding agent. It is published closed-source; use the
8
+ CLI directly unless you have a written integration agreement.
9
+
10
+ Node 22+ required.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Context compaction. Keep a long session inside the model's context window.
3
+ *
4
+ * Without this, a session that runs long enough eventually sends more tokens
5
+ * than the model accepts and every subsequent turn fails. Compaction summarises
6
+ * the older part of the conversation into one message and keeps recent turns
7
+ * verbatim, so work continues instead of dying.
8
+ *
9
+ * The subtle constraint is **tool-call pairing**: an assistant message carrying
10
+ * `tool_calls` must be followed by a `tool` message for every one of those ids.
11
+ * Cutting the history at an arbitrary index orphans tool results (or leaves
12
+ * dangling calls) and the provider rejects the request outright. Everything here
13
+ * cuts only at boundaries where the remainder is self-contained.
14
+ */
15
+ import type { ChatMessage } from "./model/openrouter.js";
16
+ /** Fallback when the model's real context length is unknown. */
17
+ export declare const DEFAULT_CONTEXT_TOKENS = 200000;
18
+ /** Compact once estimated usage passes this share of the window. */
19
+ export declare const COMPACT_THRESHOLD = 0.7;
20
+ /**
21
+ * Rough token estimate: ~4 chars per token.
22
+ *
23
+ * Deliberately not a real tokenizer. That would mean shipping model-specific
24
+ * vocabularies for 18 models. This drives a *threshold*, and erring high just
25
+ * compacts slightly early, which is harmless.
26
+ */
27
+ export declare function estimateTokens(messages: ChatMessage[]): number;
28
+ /**
29
+ * Pick the latest safe cut point that still leaves a useful amount of recent
30
+ * history verbatim.
31
+ *
32
+ * Walks backwards accumulating tokens until the kept tail fills its share of
33
+ * the window, then snaps *backwards* to the nearest safe boundary. Snapping
34
+ * backwards (keeping more) rather than forwards guarantees we never cut into an
35
+ * exchange to hit a token target.
36
+ *
37
+ * Returns 0 when no safe boundary exists. Nothing is compacted rather than
38
+ * risking a malformed request.
39
+ */
40
+ export declare function findCutpoint(messages: ChatMessage[], contextTokens?: number): number;
41
+ /** Render a slice as plain text for the summariser. */
42
+ export declare function renderTranscript(messages: ChatMessage[]): string;
43
+ /**
44
+ * The instruction given to the summariser model.
45
+ *
46
+ * BYOK only. In platform mode the API assembles this from `context.mode`
47
+ * ("compaction"), the same rule as every other prompt (PLAN.md §1.3), and the
48
+ * client sends the bare transcript. Keep the two in step: the copy the server
49
+ * uses lives in `api/src/prompts/system.ts`.
50
+ */
51
+ export declare function summaryPrompt(transcript: string): string;
52
+ export interface CompactionPlan {
53
+ /** Messages to summarise (already excludes the system prefix). */
54
+ toSummarise: ChatMessage[];
55
+ /** Messages kept verbatim after the summary. */
56
+ kept: ChatMessage[];
57
+ /** Leading system messages, preserved as-is. */
58
+ systemPrefix: ChatMessage[];
59
+ beforeTokens: number;
60
+ }
61
+ /**
62
+ * Decide what to compact. Returns null when compaction isn't possible or worth
63
+ * it. The caller should carry on unchanged.
64
+ */
65
+ export declare function planCompaction(messages: ChatMessage[], contextTokens?: number): CompactionPlan | null;
66
+ /** Rebuild the message list around a completed summary. */
67
+ export declare function applyCompaction(plan: CompactionPlan, summary: string): ChatMessage[];
68
+ /** Should we compact before the next request? */
69
+ export declare function shouldCompact(messages: ChatMessage[], contextTokens: number): boolean;
@@ -0,0 +1,174 @@
1
+ /** Fallback when the model's real context length is unknown. */
2
+ export const DEFAULT_CONTEXT_TOKENS = 200_000;
3
+ /** Compact once estimated usage passes this share of the window. */
4
+ export const COMPACT_THRESHOLD = 0.7;
5
+ /**
6
+ * Share of the window reserved for verbatim recent history. The rest is
7
+ * summarised. Token-based rather than a message count: a session can be six
8
+ * messages long and still overflow (reading a few large files does it), and a
9
+ * fixed "keep the last N messages" rule can never compact that case.
10
+ */
11
+ const KEEP_RECENT_SHARE = 0.3;
12
+ /** Always keep at least this many messages verbatim, however large they are. */
13
+ const KEEP_RECENT_MIN = 4;
14
+ /** Never compact below this many messages; there's nothing worth summarising. */
15
+ const MIN_TO_COMPACT = 4;
16
+ /**
17
+ * Rough token estimate: ~4 chars per token.
18
+ *
19
+ * Deliberately not a real tokenizer. That would mean shipping model-specific
20
+ * vocabularies for 18 models. This drives a *threshold*, and erring high just
21
+ * compacts slightly early, which is harmless.
22
+ */
23
+ export function estimateTokens(messages) {
24
+ let chars = 0;
25
+ for (const m of messages) {
26
+ chars += typeof m.content === "string" ? m.content.length : 0;
27
+ if (m.role === "assistant" && m.tool_calls) {
28
+ for (const tc of m.tool_calls) {
29
+ chars += tc.function.name.length + tc.function.arguments.length;
30
+ }
31
+ }
32
+ }
33
+ return Math.ceil(chars / 4);
34
+ }
35
+ /**
36
+ * True when `messages[0..index)` can be cut away without orphaning anything -
37
+ * i.e. `messages[index]` starts a fresh exchange rather than continuing one.
38
+ *
39
+ * A `tool` message always belongs to the assistant turn before it, so a cut
40
+ * landing on one would strand it. An assistant message may itself be the reply
41
+ * being answered by following tool results, so only `user` messages (and the
42
+ * very end) are safe boundaries.
43
+ */
44
+ function isSafeCut(messages, index) {
45
+ if (index <= 0 || index >= messages.length)
46
+ return false;
47
+ return messages[index].role === "user";
48
+ }
49
+ /**
50
+ * Pick the latest safe cut point that still leaves a useful amount of recent
51
+ * history verbatim.
52
+ *
53
+ * Walks backwards accumulating tokens until the kept tail fills its share of
54
+ * the window, then snaps *backwards* to the nearest safe boundary. Snapping
55
+ * backwards (keeping more) rather than forwards guarantees we never cut into an
56
+ * exchange to hit a token target.
57
+ *
58
+ * Returns 0 when no safe boundary exists. Nothing is compacted rather than
59
+ * risking a malformed request.
60
+ */
61
+ export function findCutpoint(messages, contextTokens = DEFAULT_CONTEXT_TOKENS) {
62
+ const keepBudget = Math.max(1, contextTokens * KEEP_RECENT_SHARE);
63
+ let kept = 0;
64
+ let boundary = messages.length;
65
+ for (let i = messages.length - 1; i > 0; i--) {
66
+ kept += estimateTokens([messages[i]]);
67
+ const keptEnough = messages.length - i >= KEEP_RECENT_MIN;
68
+ if (kept >= keepBudget && keptEnough) {
69
+ boundary = i;
70
+ break;
71
+ }
72
+ boundary = i;
73
+ }
74
+ // Snap backwards to a boundary that leaves the tail self-contained.
75
+ for (let i = boundary; i > 0; i--) {
76
+ if (isSafeCut(messages, i))
77
+ return i;
78
+ }
79
+ return 0;
80
+ }
81
+ /** Leading system messages are configuration, never conversation. Never cut. */
82
+ function systemPrefixLength(messages) {
83
+ let i = 0;
84
+ while (i < messages.length && messages[i].role === "system")
85
+ i++;
86
+ return i;
87
+ }
88
+ /** Render a slice as plain text for the summariser. */
89
+ export function renderTranscript(messages) {
90
+ const lines = [];
91
+ for (const m of messages) {
92
+ if (m.role === "system")
93
+ continue;
94
+ if (m.role === "user") {
95
+ lines.push(`USER: ${m.content}`);
96
+ }
97
+ else if (m.role === "assistant") {
98
+ if (m.content)
99
+ lines.push(`ASSISTANT: ${m.content}`);
100
+ for (const tc of m.tool_calls ?? []) {
101
+ lines.push(`ASSISTANT CALLED ${tc.function.name}(${truncate(tc.function.arguments, 400)})`);
102
+ }
103
+ }
104
+ else {
105
+ lines.push(`TOOL RESULT: ${truncate(m.content, 800)}`);
106
+ }
107
+ }
108
+ return lines.join("\n");
109
+ }
110
+ function truncate(s, max) {
111
+ return s.length > max ? `${s.slice(0, max)}…[truncated]` : s;
112
+ }
113
+ /**
114
+ * The instruction given to the summariser model.
115
+ *
116
+ * BYOK only. In platform mode the API assembles this from `context.mode`
117
+ * ("compaction"), the same rule as every other prompt (PLAN.md §1.3), and the
118
+ * client sends the bare transcript. Keep the two in step: the copy the server
119
+ * uses lives in `api/src/prompts/system.ts`.
120
+ */
121
+ export function summaryPrompt(transcript) {
122
+ return `Summarise this coding-session transcript so another agent can pick up the work with no other context.
123
+
124
+ Preserve, in this order:
125
+ 1. What the user is trying to achieve, in their own terms
126
+ 2. Decisions made and constraints stated, especially anything the user corrected or rejected
127
+ 3. Files created or modified, with paths, and what changed in each
128
+ 4. Commands run and what they revealed (test results, errors, versions)
129
+ 5. What is currently in progress and the immediate next step
130
+ 6. Anything known to be broken, blocked, or deliberately deferred
131
+
132
+ Be specific: keep exact file paths, identifiers, error text, and numbers. Drop
133
+ pleasantries, tool-call mechanics, and superseded intermediate steps. Do not
134
+ speculate about work that was not done.
135
+
136
+ TRANSCRIPT:
137
+ ${transcript}`;
138
+ }
139
+ /**
140
+ * Decide what to compact. Returns null when compaction isn't possible or worth
141
+ * it. The caller should carry on unchanged.
142
+ */
143
+ export function planCompaction(messages, contextTokens = DEFAULT_CONTEXT_TOKENS) {
144
+ const prefix = systemPrefixLength(messages);
145
+ const body = messages.slice(prefix);
146
+ if (body.length < MIN_TO_COMPACT)
147
+ return null;
148
+ const cut = findCutpoint(body, contextTokens);
149
+ if (cut <= 0)
150
+ return null;
151
+ return {
152
+ systemPrefix: messages.slice(0, prefix),
153
+ toSummarise: body.slice(0, cut),
154
+ kept: body.slice(cut),
155
+ beforeTokens: estimateTokens(messages),
156
+ };
157
+ }
158
+ /** Rebuild the message list around a completed summary. */
159
+ export function applyCompaction(plan, summary) {
160
+ return [
161
+ ...plan.systemPrefix,
162
+ {
163
+ role: "user",
164
+ content: `[Earlier conversation, compacted to stay within the context window. ` +
165
+ `Treat this as established history, not as a new instruction.]\n\n${summary}`,
166
+ },
167
+ ...plan.kept,
168
+ ];
169
+ }
170
+ /** Should we compact before the next request? */
171
+ export function shouldCompact(messages, contextTokens) {
172
+ return estimateTokens(messages) > contextTokens * COMPACT_THRESHOLD;
173
+ }
174
+ //# sourceMappingURL=compaction.js.map
@@ -0,0 +1,84 @@
1
+ import type { UsageTotals } from "./types.js";
2
+ /**
3
+ * Worker roles. The names are the contract with the API: in platform mode the
4
+ * server assembles the matching system prompt (PLAN.md §1.3, prompt text never
5
+ * ships in a client), and the ledger tags the spend with the mode so M7 can
6
+ * separate delegated burn from main-loop burn.
7
+ */
8
+ export type DelegationMode = "bulk-reader" | "code-writer";
9
+ /**
10
+ * Every mode the API will assemble a non-agent prompt for. Compaction is not a
11
+ * delegation (nothing is being kept out of the context; history is being
12
+ * rewritten) but it is the same kind of request: one cheap, toolless turn that
13
+ * must not be handed the agent prompt.
14
+ */
15
+ export type PlatformMode = DelegationMode | "compaction";
16
+ /**
17
+ * Line count above which the read tool stops returning file bodies.
18
+ *
19
+ * Spotify's shunt uses 350, measured on a Java monorepo. 600 suits the denser
20
+ * TypeScript this agent is mostly pointed at: it still catches the files that
21
+ * dominate a context window while leaving ordinary modules directly readable,
22
+ * which matters because a blocked read costs a round trip before any edit.
23
+ */
24
+ export declare const DELEGATION_MIN_LINES = 600;
25
+ /**
26
+ * Ceiling on one delegated request. Well inside an economy model's window, and
27
+ * a corpus this large is a sign the caller should have grepped first.
28
+ */
29
+ export declare const MAX_CORPUS_CHARS = 600000;
30
+ /**
31
+ * BYOK fallback prompts. In platform mode these are never sent: the API strips
32
+ * client system messages and assembles its own from the mode name. They exist
33
+ * so a user on their own OpenRouter key gets the same worker behaviour.
34
+ *
35
+ * Both prompts are written to suppress prose. The instruction that saves the
36
+ * most is "no markdown fences" on the writer: without it the worker wraps its
37
+ * output in formatting and commentary that the caller then has to read and
38
+ * strip, which puts the payload straight back into the context this exists to
39
+ * protect.
40
+ */
41
+ export declare const WORKER_PROMPTS: Record<DelegationMode, string>;
42
+ export interface DelegationResult {
43
+ text: string;
44
+ usage: UsageTotals;
45
+ }
46
+ /**
47
+ * One ephemeral worker turn. Toolless by construction: a worker that could call
48
+ * tools would be a sub-agent, with a sub-agent's cost.
49
+ */
50
+ export declare function delegate(opts: {
51
+ apiKey: string;
52
+ model: string;
53
+ mode: DelegationMode;
54
+ message: string;
55
+ cwd: string;
56
+ sessionId: string;
57
+ platform?: {
58
+ baseUrl: string;
59
+ };
60
+ signal?: AbortSignal;
61
+ }): Promise<DelegationResult>;
62
+ /**
63
+ * Wrap each file in a tagged block so the worker sees clear boundaries, and
64
+ * number the lines so it can cite positions the caller can then read directly.
65
+ * Those citations are what make a delegated read usable as the setup for a
66
+ * targeted edit rather than a dead end.
67
+ */
68
+ export declare function buildReadCorpus(files: Array<{
69
+ path: string;
70
+ content: string;
71
+ }>, question: string): string;
72
+ /** Spec first, then the references whose conventions the output must match. */
73
+ export declare function buildWriteCorpus(spec: string, references: Array<{
74
+ path: string;
75
+ content: string;
76
+ }>): string;
77
+ /**
78
+ * Workers wrap output in fences despite being told not to, often enough that
79
+ * stripping them is cheaper than a retry. Only whole fence lines go: a fence
80
+ * indented inside a string literal or a docstring is content.
81
+ */
82
+ export declare function stripFences(s: string): string;
83
+ /** Rough parity with `estimateTokens`, for reporting what stayed out of context. */
84
+ export declare function estimateCorpusTokens(chars: number): number;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Delegation: I/O-heavy work runs on an economy worker model, in its own
3
+ * request, and only the answer comes back into the session's context.
4
+ *
5
+ * The premise is that most of what the agent does is not reasoning. Reading a
6
+ * 4,000-line file to answer one question about it, or writing the twentieth
7
+ * test that copies the nineteen next to it, is transport. Paying frontier
8
+ * rates for it is bad enough once; the file is then resent on every following
9
+ * turn, so an 8k-token read is an 8k-token tax for the rest of the session.
10
+ *
11
+ * Two things make this different from spawning an explore sub-agent:
12
+ *
13
+ * 1. A sub-agent holds a conversation and returns prose. A delegation is one
14
+ * shot: one request, structured bullets or a file on disk, nothing kept.
15
+ * 2. The corpus never enters the parent's history at all, so asking a second
16
+ * question about the same files costs the parent nothing.
17
+ *
18
+ * Enforcement lives in the read tool (see `tools/fs-tools.ts`), not in the
19
+ * system prompt. Advice about which tool to use is a suggestion the model is
20
+ * free to skip on a busy turn; a tool that refuses to return the body is not.
21
+ *
22
+ * Worker burn is billed like any other request and rolls into the session's
23
+ * usage, so a delegation is cheaper, never free. Below `DELEGATION_MIN_LINES`
24
+ * the round trip costs more than the tokens it saves.
25
+ */
26
+ import { streamChat } from "./model/openrouter.js";
27
+ /**
28
+ * Line count above which the read tool stops returning file bodies.
29
+ *
30
+ * Spotify's shunt uses 350, measured on a Java monorepo. 600 suits the denser
31
+ * TypeScript this agent is mostly pointed at: it still catches the files that
32
+ * dominate a context window while leaving ordinary modules directly readable,
33
+ * which matters because a blocked read costs a round trip before any edit.
34
+ */
35
+ export const DELEGATION_MIN_LINES = 600;
36
+ /**
37
+ * Ceiling on one delegated request. Well inside an economy model's window, and
38
+ * a corpus this large is a sign the caller should have grepped first.
39
+ */
40
+ export const MAX_CORPUS_CHARS = 600_000;
41
+ /**
42
+ * BYOK fallback prompts. In platform mode these are never sent: the API strips
43
+ * client system messages and assembles its own from the mode name. They exist
44
+ * so a user on their own OpenRouter key gets the same worker behaviour.
45
+ *
46
+ * Both prompts are written to suppress prose. The instruction that saves the
47
+ * most is "no markdown fences" on the writer: without it the worker wraps its
48
+ * output in formatting and commentary that the caller then has to read and
49
+ * strip, which puts the payload straight back into the context this exists to
50
+ * protect.
51
+ */
52
+ export const WORKER_PROMPTS = {
53
+ "bulk-reader": "You are a precise code analyst. Read the provided files and answer the question concisely. Output structured bullets only. No greetings, no prose, no preamble, no closing summary. Lead every bullet with the exact name, type, or line number. Use nested bullets for detail. Cite line numbers as path:line, taken from the numbers in the input. Skip anything the caller did not ask for. If the files do not answer the question, say so in one bullet rather than guessing.",
54
+ "code-writer": "You generate code files from a spec and reference files. Match the reference's patterns, conventions, naming, imports, and style exactly. Output only the code: no explanation, no commentary, no markdown fences. If the spec is ambiguous, choose whatever matches the reference.",
55
+ };
56
+ /**
57
+ * One ephemeral worker turn. Toolless by construction: a worker that could call
58
+ * tools would be a sub-agent, with a sub-agent's cost.
59
+ */
60
+ export async function delegate(opts) {
61
+ if (opts.message.length > MAX_CORPUS_CHARS) {
62
+ throw new Error(`request is ${opts.message.length} chars, over the ${MAX_CORPUS_CHARS} limit. Send fewer or smaller files.`);
63
+ }
64
+ // Platform mode: the server writes the system prompt from `context.mode`.
65
+ // BYOK: there is no server, so carry the prompt locally.
66
+ const messages = opts.platform
67
+ ? [{ role: "user", content: opts.message }]
68
+ : [
69
+ { role: "system", content: WORKER_PROMPTS[opts.mode] },
70
+ { role: "user", content: opts.message },
71
+ ];
72
+ let text = "";
73
+ let usage = { inputTokens: 0, cachedTokens: 0, outputTokens: 0, requests: 0 };
74
+ for await (const ev of streamChat({
75
+ apiKey: opts.apiKey,
76
+ model: opts.model,
77
+ messages,
78
+ tools: [],
79
+ ...(opts.signal ? { signal: opts.signal } : {}),
80
+ ...(opts.platform
81
+ ? {
82
+ baseUrl: opts.platform.baseUrl,
83
+ extraBody: {
84
+ context: { cwd: opts.cwd, sessionId: opts.sessionId, mode: opts.mode },
85
+ },
86
+ }
87
+ : {}),
88
+ })) {
89
+ if (ev.type === "done") {
90
+ text = ev.result.message.content ?? "";
91
+ usage = ev.result.usage;
92
+ }
93
+ }
94
+ if (!text.trim())
95
+ throw new Error(`the ${opts.mode} worker returned nothing`);
96
+ return { text: text.trim(), usage };
97
+ }
98
+ /**
99
+ * Wrap each file in a tagged block so the worker sees clear boundaries, and
100
+ * number the lines so it can cite positions the caller can then read directly.
101
+ * Those citations are what make a delegated read usable as the setup for a
102
+ * targeted edit rather than a dead end.
103
+ */
104
+ export function buildReadCorpus(files, question) {
105
+ const blocks = files.map((f) => {
106
+ const numbered = f.content
107
+ .split("\n")
108
+ .map((line, i) => `${i + 1}\t${line}`)
109
+ .join("\n");
110
+ return `<file path="${f.path}">\n${numbered}\n</file>`;
111
+ });
112
+ return `${blocks.join("\n\n")}\n\nQuestion: ${question}\n`;
113
+ }
114
+ /** Spec first, then the references whose conventions the output must match. */
115
+ export function buildWriteCorpus(spec, references) {
116
+ const blocks = references.map((r) => `<reference path="${r.path}">\n${r.content}\n</reference>`);
117
+ return `Spec: ${spec}\n\n${blocks.join("\n\n")}\n`;
118
+ }
119
+ /**
120
+ * Workers wrap output in fences despite being told not to, often enough that
121
+ * stripping them is cheaper than a retry. Only whole fence lines go: a fence
122
+ * indented inside a string literal or a docstring is content.
123
+ */
124
+ export function stripFences(s) {
125
+ return s
126
+ .split("\n")
127
+ .filter((line) => !/^```/.test(line))
128
+ .join("\n")
129
+ .trim();
130
+ }
131
+ /** Rough parity with `estimateTokens`, for reporting what stayed out of context. */
132
+ export function estimateCorpusTokens(chars) {
133
+ return Math.ceil(chars / 4);
134
+ }
135
+ //# sourceMappingURL=delegate.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @9thprotocol/agent-core, the agent engine.
3
+ * Loop + core tools + permission modes + streaming (M1); platform transport (M2);
4
+ * sub-agents, ask_user, 9P.md memory, skills, MCP client (M3).
5
+ * Next: Auto router, context compaction, Ink TUI/VS Code hosts (M5).
6
+ */
7
+ export declare const AGENT_CORE_VERSION = "0.1.0";
8
+ export { AgentSession } from "./session.js";
9
+ export { CORE_TOOLS } from "./tools/registry.js";
10
+ export { McpManager } from "./mcp.js";
11
+ export { loadSkills, skillMessage, type Skill } from "./skills.js";
12
+ export { loadMemory } from "./memory.js";
13
+ export { loadProjectConfig, resolveVault, saveProjectConfig, scaffoldVault, vaultProtocol, type ProjectConfig, } from "./vault.js";
14
+ export { ApiError } from "./model/openrouter.js";
15
+ export { DELEGATION_MIN_LINES, MAX_CORPUS_CHARS, WORKER_PROMPTS, buildReadCorpus, buildWriteCorpus, delegate, estimateCorpusTokens, stripFences, type DelegationMode, type DelegationResult, type PlatformMode, } from "./delegate.js";
16
+ export { COMPACT_THRESHOLD, DEFAULT_CONTEXT_TOKENS, estimateTokens, planCompaction, shouldCompact, } from "./compaction.js";
17
+ export { AUTO_MODEL, classify, route, workerModel, type RouteDecision, type RouteInput, type RouterBias, type RouterCandidate, type TaskComplexity, } from "./model/router.js";
18
+ export type { AgentEvent, AskUserHandler, DelegationTotals, AskUserQuestion, PermissionDecider, PermissionMode, PermissionRequest, SessionOptions, ToolCallRequest, UsageTotals, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @9thprotocol/agent-core, the agent engine.
3
+ * Loop + core tools + permission modes + streaming (M1); platform transport (M2);
4
+ * sub-agents, ask_user, 9P.md memory, skills, MCP client (M3).
5
+ * Next: Auto router, context compaction, Ink TUI/VS Code hosts (M5).
6
+ */
7
+ export const AGENT_CORE_VERSION = "0.1.0";
8
+ export { AgentSession } from "./session.js";
9
+ export { CORE_TOOLS } from "./tools/registry.js";
10
+ export { McpManager } from "./mcp.js";
11
+ export { loadSkills, skillMessage } from "./skills.js";
12
+ export { loadMemory } from "./memory.js";
13
+ export { loadProjectConfig, resolveVault, saveProjectConfig, scaffoldVault, vaultProtocol, } from "./vault.js";
14
+ export { ApiError } from "./model/openrouter.js";
15
+ export { DELEGATION_MIN_LINES, MAX_CORPUS_CHARS, WORKER_PROMPTS, buildReadCorpus, buildWriteCorpus, delegate, estimateCorpusTokens, stripFences, } from "./delegate.js";
16
+ export { COMPACT_THRESHOLD, DEFAULT_CONTEXT_TOKENS, estimateTokens, planCompaction, shouldCompact, } from "./compaction.js";
17
+ export { AUTO_MODEL, classify, route, workerModel, } from "./model/router.js";
18
+ //# sourceMappingURL=index.js.map
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { ToolDef } from "./tools/types.js";
2
+ /**
3
+ * Connects configured stdio MCP servers and exposes their tools as
4
+ * `mcp__<server>__<tool>` ToolDefs (kind "exec", permission-gated like bash).
5
+ */
6
+ export declare class McpManager {
7
+ readonly tools: ToolDef[];
8
+ readonly servers: string[];
9
+ private clients;
10
+ static fromCwd(cwd: string): Promise<McpManager>;
11
+ private connect;
12
+ close(): Promise<void>;
13
+ }
package/dist/mcp.js ADDED
@@ -0,0 +1,78 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
6
+ /** Merge ~/.9p/mcp.json and <cwd>/.9p/mcp.json (project wins per server name). */
7
+ function loadConfig(cwd) {
8
+ const merged = {};
9
+ for (const file of [
10
+ path.join(os.homedir(), ".9p", "mcp.json"),
11
+ path.join(cwd, ".9p", "mcp.json"),
12
+ ]) {
13
+ try {
14
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
15
+ Object.assign(merged, parsed.mcpServers ?? {});
16
+ }
17
+ catch {
18
+ // absent or invalid, skip
19
+ }
20
+ }
21
+ return merged;
22
+ }
23
+ /**
24
+ * Connects configured stdio MCP servers and exposes their tools as
25
+ * `mcp__<server>__<tool>` ToolDefs (kind "exec", permission-gated like bash).
26
+ */
27
+ export class McpManager {
28
+ tools = [];
29
+ servers = [];
30
+ clients = [];
31
+ static async fromCwd(cwd) {
32
+ const manager = new McpManager();
33
+ for (const [name, cfg] of Object.entries(loadConfig(cwd))) {
34
+ try {
35
+ await manager.connect(name, cfg);
36
+ }
37
+ catch (err) {
38
+ console.error(`mcp: failed to connect "${name}": ${err instanceof Error ? err.message : err}`);
39
+ }
40
+ }
41
+ return manager;
42
+ }
43
+ async connect(serverName, cfg) {
44
+ const client = new Client({ name: "9p", version: "0.1.0" });
45
+ const transport = new StdioClientTransport({
46
+ command: cfg.command,
47
+ args: cfg.args ?? [],
48
+ env: { ...process.env, ...cfg.env },
49
+ });
50
+ await client.connect(transport);
51
+ this.clients.push(client);
52
+ this.servers.push(serverName);
53
+ const { tools } = await client.listTools();
54
+ for (const t of tools) {
55
+ this.tools.push({
56
+ name: `mcp__${serverName}__${t.name}`,
57
+ description: t.description ?? `${t.name} (MCP: ${serverName})`,
58
+ kind: "exec",
59
+ parameters: t.inputSchema ?? { type: "object" },
60
+ summarize: (input) => `mcp ${serverName}.${t.name}(${JSON.stringify(input).slice(0, 80)})`,
61
+ run: async (input) => {
62
+ const result = await client.callTool({ name: t.name, arguments: input });
63
+ const content = (result.content ?? []);
64
+ const text = content
65
+ .map((c) => (c.type === "text" ? (c.text ?? "") : `[${c.type}]`))
66
+ .join("\n");
67
+ if (result.isError)
68
+ throw new Error(text || "MCP tool error");
69
+ return text || "[no output]";
70
+ },
71
+ });
72
+ }
73
+ }
74
+ async close() {
75
+ await Promise.allSettled(this.clients.map((c) => c.close()));
76
+ }
77
+ }
78
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Memory context loaded every session: 9P.md standing instructions (global
3
+ * ~/.9p/9P.md, then project 9P.md, project wins by appearing last), plus the
4
+ * vault protocol when the project links an Obsidian-style vault. The vault's
5
+ * *content* is never bulk-loaded, the agent navigates it with tools.
6
+ */
7
+ export declare function loadMemory(cwd: string): string;
package/dist/memory.js ADDED
@@ -0,0 +1,33 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { resolveVault, vaultProtocol } from "./vault.js";
5
+ const MEMORY_CAP = 8000; // chars, memory must never dominate the context window
6
+ /**
7
+ * Memory context loaded every session: 9P.md standing instructions (global
8
+ * ~/.9p/9P.md, then project 9P.md, project wins by appearing last), plus the
9
+ * vault protocol when the project links an Obsidian-style vault. The vault's
10
+ * *content* is never bulk-loaded, the agent navigates it with tools.
11
+ */
12
+ export function loadMemory(cwd) {
13
+ const sources = [
14
+ ["Global memory", path.join(os.homedir(), ".9p", "9P.md")],
15
+ ["Project memory", path.join(cwd, "9P.md")],
16
+ ];
17
+ const parts = [];
18
+ for (const [label, file] of sources) {
19
+ try {
20
+ const text = fs.readFileSync(file, "utf8").trim();
21
+ if (text)
22
+ parts.push(`### ${label} (${file})\n${text}`);
23
+ }
24
+ catch {
25
+ // absent, fine
26
+ }
27
+ }
28
+ const vault = resolveVault(cwd);
29
+ if (vault)
30
+ parts.push(vaultProtocol(vault));
31
+ return parts.join("\n\n").slice(0, MEMORY_CAP);
32
+ }
33
+ //# sourceMappingURL=memory.js.map