@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/dist/skills.js ADDED
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ /**
5
+ * Skills: markdown files with optional `--- name/description ---` frontmatter, loaded
6
+ * from ~/.9p/skills and <cwd>/.9p/skills (project wins on name clash). Anti-bloat rule
7
+ * (PLAN.md §4.1): a skill's body enters context only when the user invokes /<name>.
8
+ */
9
+ export function loadSkills(cwd) {
10
+ const dirs = [path.join(os.homedir(), ".9p", "skills"), path.join(cwd, ".9p", "skills")];
11
+ const byName = new Map();
12
+ for (const dir of dirs) {
13
+ let files = [];
14
+ try {
15
+ files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
16
+ }
17
+ catch {
18
+ continue;
19
+ }
20
+ for (const file of files) {
21
+ const source = path.join(dir, file);
22
+ let raw;
23
+ try {
24
+ raw = fs.readFileSync(source, "utf8");
25
+ }
26
+ catch {
27
+ continue;
28
+ }
29
+ const skill = parseSkill(raw, path.basename(file, ".md"), source);
30
+ byName.set(skill.name, skill);
31
+ }
32
+ }
33
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
34
+ }
35
+ function parseSkill(raw, fallbackName, source) {
36
+ let name = fallbackName;
37
+ let description = "";
38
+ let body = raw;
39
+ const fm = /^---\n([\s\S]*?)\n---\n?/.exec(raw);
40
+ if (fm?.[1] !== undefined) {
41
+ body = raw.slice(fm[0].length);
42
+ for (const line of fm[1].split("\n")) {
43
+ const m = /^(name|description):\s*(.+)$/.exec(line.trim());
44
+ if (m?.[1] === "name" && m[2])
45
+ name = m[2].trim();
46
+ if (m?.[1] === "description" && m[2])
47
+ description = m[2].trim();
48
+ }
49
+ }
50
+ return { name, description, body: body.trim(), source };
51
+ }
52
+ /** The message sent when a user invokes /<skill> [args]. */
53
+ export function skillMessage(skill, args) {
54
+ return `The user invoked the "${skill.name}" skill. Follow these instructions:\n\n${skill.body}${args ? `\n\nUser input for this invocation: ${args}` : ""}`;
55
+ }
56
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1,2 @@
1
+ import { type ToolDef } from "./types.js";
2
+ export declare const bashTool: ToolDef;
@@ -0,0 +1,38 @@
1
+ import { exec } from "node:child_process";
2
+ import { str, truncate } from "./types.js";
3
+ const MAX_OUTPUT = 30_000;
4
+ const DEFAULT_TIMEOUT_MS = 120_000;
5
+ const MAX_TIMEOUT_MS = 600_000;
6
+ export const bashTool = {
7
+ name: "bash",
8
+ description: "Run a shell command in the working directory and return its output. Not interactive, no prompts, no editors.",
9
+ kind: "exec",
10
+ parameters: {
11
+ type: "object",
12
+ properties: {
13
+ command: { type: "string" },
14
+ timeout_ms: { type: "number", description: `Default ${DEFAULT_TIMEOUT_MS}, max ${MAX_TIMEOUT_MS}` },
15
+ },
16
+ required: ["command"],
17
+ },
18
+ summarize: (i) => `bash: ${truncate(String(i.command ?? ""), 120).split("\n")[0]}`,
19
+ run(input, ctx) {
20
+ const command = str(input.command);
21
+ const timeout = Math.min(Number(input.timeout_ms ?? DEFAULT_TIMEOUT_MS), MAX_TIMEOUT_MS);
22
+ return new Promise((resolvePromise) => {
23
+ exec(command, { cwd: ctx.cwd, timeout, maxBuffer: 10 * 1024 * 1024, shell: "/bin/zsh" }, (err, stdout, stderr) => {
24
+ let out = stdout + (stderr ? (stdout ? "\n" : "") + stderr : "");
25
+ out = truncate(out, MAX_OUTPUT);
26
+ if (err) {
27
+ const e = err;
28
+ const reason = e.killed ? `timed out after ${timeout}ms` : `exit code ${e.code ?? "?"}`;
29
+ resolvePromise(`[${reason}]\n${out}` || `[${reason}]`);
30
+ }
31
+ else {
32
+ resolvePromise(out || "[no output]");
33
+ }
34
+ });
35
+ });
36
+ },
37
+ };
38
+ //# sourceMappingURL=bash.js.map
@@ -0,0 +1,5 @@
1
+ import { truncate, type ToolDef } from "./types.js";
2
+ export declare const readTool: ToolDef;
3
+ export declare const writeTool: ToolDef;
4
+ export declare const editTool: ToolDef;
5
+ export { truncate };
@@ -0,0 +1,115 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { str, truncate } from "./types.js";
4
+ const MAX_LINES = 2000;
5
+ const MAX_LINE_CHARS = 500;
6
+ function resolve(cwd, p) {
7
+ return path.resolve(cwd, p);
8
+ }
9
+ export const readTool = {
10
+ name: "read",
11
+ description: "Read a file. Returns numbered lines. Use offset/limit for large files; whole-file reads over the size threshold are refused and must go through bulk_read.",
12
+ kind: "read",
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ file_path: { type: "string", description: "Path (absolute or relative to cwd)" },
17
+ offset: { type: "number", description: "1-based line to start from" },
18
+ limit: { type: "number", description: "Max lines to return" },
19
+ },
20
+ required: ["file_path"],
21
+ },
22
+ summarize: (i) => `read ${i.file_path}`,
23
+ async run(input, ctx) {
24
+ const file = resolve(ctx.cwd, str(input.file_path));
25
+ const raw = await fs.readFile(file, "utf8");
26
+ const lines = raw.split("\n");
27
+ // The size gate. A targeted read is always allowed: it means the caller
28
+ // already knows which section it wants, which is the case delegation
29
+ // cannot serve, since editing needs exact bytes and a worker's summary
30
+ // carries no reliable ones.
31
+ //
32
+ // This refuses before `readFiles.add`, deliberately. Marking a file read
33
+ // here would let write/edit clear their read-before-modify check on a body
34
+ // nobody ever saw.
35
+ const targeted = input.offset !== undefined || input.limit !== undefined;
36
+ if (ctx.delegation && !targeted && lines.length > ctx.delegation.minLines) {
37
+ return (`Refused: ${file} is ${lines.length} lines (threshold ${ctx.delegation.minLines}). ` +
38
+ `Reading it whole would cost roughly ${Math.ceil(raw.length / 4)} tokens of context, on this turn and every turn after it.\n` +
39
+ `- To learn something from it: bulk_read with a specific question. The file goes to a worker model, not into this conversation, so follow-up questions about the same paths are free.\n` +
40
+ `- To edit it: read again with offset/limit for the section you need. Targeted reads are never refused. Use grep first to find the line.`);
41
+ }
42
+ ctx.readFiles.add(file);
43
+ const offset = Math.max(1, Number(input.offset ?? 1));
44
+ const limit = Math.min(Number(input.limit ?? MAX_LINES), MAX_LINES);
45
+ const slice = lines.slice(offset - 1, offset - 1 + limit);
46
+ const body = slice
47
+ .map((l, i) => `${offset + i}\t${l.length > MAX_LINE_CHARS ? l.slice(0, MAX_LINE_CHARS) + "…" : l}`)
48
+ .join("\n");
49
+ const remaining = lines.length - (offset - 1 + slice.length);
50
+ return remaining > 0 ? `${body}\n… [${remaining} more lines. Use offset=${offset + slice.length}]` : body;
51
+ },
52
+ };
53
+ export const writeTool = {
54
+ name: "write",
55
+ description: "Create or overwrite a file. Overwriting an existing file requires reading it first.",
56
+ kind: "mutate",
57
+ parameters: {
58
+ type: "object",
59
+ properties: {
60
+ file_path: { type: "string" },
61
+ content: { type: "string" },
62
+ },
63
+ required: ["file_path", "content"],
64
+ },
65
+ summarize: (i) => `write ${i.file_path}`,
66
+ async run(input, ctx) {
67
+ const file = resolve(ctx.cwd, str(input.file_path));
68
+ const exists = await fs.access(file).then(() => true, () => false);
69
+ if (exists && !ctx.readFiles.has(file)) {
70
+ throw new Error(`${file} exists. Read it before overwriting`);
71
+ }
72
+ await fs.mkdir(path.dirname(file), { recursive: true });
73
+ const content = str(input.content);
74
+ await fs.writeFile(file, content, "utf8");
75
+ ctx.readFiles.add(file);
76
+ return `Wrote ${content.split("\n").length} lines to ${file}`;
77
+ },
78
+ };
79
+ export const editTool = {
80
+ name: "edit",
81
+ description: "Replace an exact string in a file (must be unique unless replace_all). Read the file first; match its exact indentation.",
82
+ kind: "mutate",
83
+ parameters: {
84
+ type: "object",
85
+ properties: {
86
+ file_path: { type: "string" },
87
+ old_string: { type: "string" },
88
+ new_string: { type: "string" },
89
+ replace_all: { type: "boolean" },
90
+ },
91
+ required: ["file_path", "old_string", "new_string"],
92
+ },
93
+ summarize: (i) => `edit ${i.file_path}`,
94
+ async run(input, ctx) {
95
+ const file = resolve(ctx.cwd, str(input.file_path));
96
+ if (!ctx.readFiles.has(file))
97
+ throw new Error(`Read ${file} before editing it`);
98
+ const oldStr = str(input.old_string);
99
+ const newStr = str(input.new_string);
100
+ if (oldStr === newStr)
101
+ throw new Error("old_string and new_string are identical");
102
+ const raw = await fs.readFile(file, "utf8");
103
+ const count = raw.split(oldStr).length - 1;
104
+ if (count === 0)
105
+ throw new Error(`old_string not found in ${file}, re-read the file; match exact text`);
106
+ if (count > 1 && !input.replace_all) {
107
+ throw new Error(`old_string appears ${count} times. Add more context to make it unique, or set replace_all`);
108
+ }
109
+ const next = input.replace_all ? raw.split(oldStr).join(newStr) : raw.replace(oldStr, newStr);
110
+ await fs.writeFile(file, next, "utf8");
111
+ return `Replaced ${input.replace_all ? count : 1} occurrence(s) in ${file}`;
112
+ },
113
+ };
114
+ export { truncate };
115
+ //# sourceMappingURL=fs-tools.js.map
@@ -0,0 +1,5 @@
1
+ import type { ToolDef } from "./types.js";
2
+ import type { ToolSchema } from "../model/openrouter.js";
3
+ export declare const CORE_TOOLS: ToolDef[];
4
+ export declare function getTool(name: string): ToolDef | undefined;
5
+ export declare function toolSchemas(): ToolSchema[];
@@ -0,0 +1,12 @@
1
+ import { toSchema } from "./types.js";
2
+ import { readTool, writeTool, editTool } from "./fs-tools.js";
3
+ import { globTool, grepTool } from "./search-tools.js";
4
+ import { bashTool } from "./bash.js";
5
+ export const CORE_TOOLS = [readTool, writeTool, editTool, globTool, grepTool, bashTool];
6
+ export function getTool(name) {
7
+ return CORE_TOOLS.find((t) => t.name === name);
8
+ }
9
+ export function toolSchemas() {
10
+ return CORE_TOOLS.map(toSchema);
11
+ }
12
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,3 @@
1
+ import { type ToolDef } from "./types.js";
2
+ export declare const globTool: ToolDef;
3
+ export declare const grepTool: ToolDef;
@@ -0,0 +1,84 @@
1
+ import { glob as fsGlob } from "node:fs/promises";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { str, truncate } from "./types.js";
5
+ const pExecFile = promisify(execFile);
6
+ const SKIP = /(^|\/)(node_modules|\.git|dist|build|\.next)(\/|$)/;
7
+ export const globTool = {
8
+ name: "glob",
9
+ description: 'Find files by glob pattern, e.g. "**/*.ts" or "src/**/config.*". Returns paths relative to cwd.',
10
+ kind: "read",
11
+ parameters: {
12
+ type: "object",
13
+ properties: {
14
+ pattern: { type: "string" },
15
+ path: { type: "string", description: "Base directory (default: cwd)" },
16
+ },
17
+ required: ["pattern"],
18
+ },
19
+ summarize: (i) => `glob ${i.pattern}`,
20
+ async run(input, ctx) {
21
+ const results = [];
22
+ for await (const entry of fsGlob(str(input.pattern), {
23
+ cwd: input.path ? String(input.path) : ctx.cwd,
24
+ })) {
25
+ if (SKIP.test(entry))
26
+ continue;
27
+ results.push(entry);
28
+ if (results.length >= 500)
29
+ break;
30
+ }
31
+ if (!results.length)
32
+ return "No files matched.";
33
+ results.sort();
34
+ return results.join("\n") + (results.length >= 500 ? "\n… [capped at 500]" : "");
35
+ },
36
+ };
37
+ export const grepTool = {
38
+ name: "grep",
39
+ description: "Search file contents with a regex (POSIX extended). Returns file:line:text matches.",
40
+ kind: "read",
41
+ parameters: {
42
+ type: "object",
43
+ properties: {
44
+ pattern: { type: "string" },
45
+ path: { type: "string", description: "File or directory to search (default: cwd)" },
46
+ include: { type: "string", description: 'Only files matching this glob, e.g. "*.ts"' },
47
+ case_insensitive: { type: "boolean" },
48
+ },
49
+ required: ["pattern"],
50
+ },
51
+ summarize: (i) => `grep ${i.pattern}`,
52
+ async run(input, ctx) {
53
+ const args = [
54
+ "-rnE",
55
+ "-I",
56
+ "--exclude-dir=node_modules",
57
+ "--exclude-dir=.git",
58
+ "--exclude-dir=dist",
59
+ "--exclude-dir=build",
60
+ ];
61
+ if (input.case_insensitive)
62
+ args.push("-i");
63
+ if (input.include)
64
+ args.push(`--include=${String(input.include)}`);
65
+ args.push("-e", str(input.pattern), input.path ? String(input.path) : ".");
66
+ try {
67
+ const { stdout } = await pExecFile("grep", args, {
68
+ cwd: ctx.cwd,
69
+ maxBuffer: 10 * 1024 * 1024,
70
+ });
71
+ const lines = stdout.trimEnd().split("\n");
72
+ const capped = lines.slice(0, 250);
73
+ return (capped.map((l) => truncate(l, 400)).join("\n") +
74
+ (lines.length > 250 ? `\n… [${lines.length - 250} more matches]` : ""));
75
+ }
76
+ catch (err) {
77
+ const e = err;
78
+ if (e.code === 1)
79
+ return "No matches.";
80
+ throw new Error(`grep failed: ${e.message}`);
81
+ }
82
+ },
83
+ };
84
+ //# sourceMappingURL=search-tools.js.map
@@ -0,0 +1,27 @@
1
+ import type { ToolSchema } from "../model/openrouter.js";
2
+ /** read: always auto-allowed · mutate: file changes · exec: arbitrary commands */
3
+ export type ToolKind = "read" | "mutate" | "exec";
4
+ export interface ToolContext {
5
+ cwd: string;
6
+ /** Absolute paths read this session. Write/edit require reading existing files first. */
7
+ readFiles: Set<string>;
8
+ /**
9
+ * Set when the session can delegate I/O to a worker model. Its presence is
10
+ * what arms the read tool's size gate: with no worker to redirect to, a
11
+ * refusal would leave the agent with no way to see the file at all.
12
+ */
13
+ delegation?: {
14
+ minLines: number;
15
+ };
16
+ }
17
+ export interface ToolDef {
18
+ name: string;
19
+ description: string;
20
+ kind: ToolKind;
21
+ parameters: Record<string, unknown>;
22
+ summarize(input: Record<string, unknown>): string;
23
+ run(input: Record<string, unknown>, ctx: ToolContext): Promise<string>;
24
+ }
25
+ export declare function toSchema(t: ToolDef): ToolSchema;
26
+ export declare function str(v: unknown): string;
27
+ export declare function truncate(s: string, max: number): string;
@@ -0,0 +1,15 @@
1
+ export function toSchema(t) {
2
+ return {
3
+ type: "function",
4
+ function: { name: t.name, description: t.description, parameters: t.parameters },
5
+ };
6
+ }
7
+ export function str(v) {
8
+ if (typeof v !== "string")
9
+ throw new Error("expected a string");
10
+ return v;
11
+ }
12
+ export function truncate(s, max) {
13
+ return s.length > max ? s.slice(0, max) + `\n… [truncated ${s.length - max} chars]` : s;
14
+ }
15
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,130 @@
1
+ export type PermissionMode = "default" | "accept-edits" | "plan" | "bypass";
2
+ export interface UsageTotals {
3
+ inputTokens: number;
4
+ cachedTokens: number;
5
+ outputTokens: number;
6
+ requests: number;
7
+ }
8
+ export interface ToolCallRequest {
9
+ id: string;
10
+ name: string;
11
+ input: Record<string, unknown>;
12
+ }
13
+ export type AgentEvent = {
14
+ type: "text_delta";
15
+ text: string;
16
+ }
17
+ /** Auto routing resolved this turn's model, hosts surface it (PLAN.md §5.5 transparency). */
18
+ | {
19
+ type: "model_selected";
20
+ model: string;
21
+ complexity: string;
22
+ reason: string;
23
+ }
24
+ /** History was summarised to stay inside the context window. */
25
+ | {
26
+ type: "compacted";
27
+ beforeTokens: number;
28
+ afterTokens: number;
29
+ } | {
30
+ type: "tool_start";
31
+ call: ToolCallRequest;
32
+ summary: string;
33
+ } | {
34
+ type: "tool_end";
35
+ call: ToolCallRequest;
36
+ output: string;
37
+ isError: boolean;
38
+ durationMs: number;
39
+ } | {
40
+ type: "permission_denied";
41
+ call: ToolCallRequest;
42
+ summary: string;
43
+ } | {
44
+ type: "turn_end";
45
+ usage: UsageTotals;
46
+ } | {
47
+ type: "error";
48
+ message: string;
49
+ code?: string;
50
+ };
51
+ export interface PermissionRequest {
52
+ tool: string;
53
+ summary: string;
54
+ input: Record<string, unknown>;
55
+ }
56
+ /** Host-provided gate for tool calls the mode doesn't auto-allow. */
57
+ export type PermissionDecider = (req: PermissionRequest) => Promise<boolean>;
58
+ export interface AskUserQuestion {
59
+ question: string;
60
+ options?: string[];
61
+ }
62
+ /** Host-provided handler for the ask_user tool. Returns the user's answer. */
63
+ export type AskUserHandler = (q: AskUserQuestion) => Promise<string>;
64
+ export interface SessionOptions {
65
+ /** OpenRouter key (BYOK), or a 9th Protocol access token when `platform` is set. */
66
+ apiKey: string;
67
+ /** A model id, or `"auto"` to route per turn (PLAN.md §5.5). See `autoRouter`. */
68
+ model: string;
69
+ cwd: string;
70
+ mode?: PermissionMode;
71
+ decide?: PermissionDecider;
72
+ askUser?: AskUserHandler;
73
+ maxTurnsPerMessage?: number;
74
+ /**
75
+ * Route through the 9th Protocol API (metered, server-assembled system prompt)
76
+ * instead of calling OpenRouter directly.
77
+ */
78
+ platform?: {
79
+ baseUrl: string;
80
+ };
81
+ /** Connected MCP servers whose tools join the session (share one manager per process). */
82
+ mcp?: import("./mcp.js").McpManager;
83
+ /** "read-only" restricts to read-kind tools (explore sub-agents). */
84
+ toolset?: "all" | "read-only";
85
+ /** Sub-agent nesting depth (internal). Task tool disappears at depth 2. */
86
+ depth?: number;
87
+ /**
88
+ * Config for `model: "auto"`. `catalog` comes from GET /v1/models so routing
89
+ * respects plan locks; omit it in BYOK mode, where every model is allowed.
90
+ */
91
+ autoRouter?: {
92
+ bias?: import("./model/router.js").RouterBias;
93
+ catalog?: import("./model/router.js").RouterCandidate[];
94
+ };
95
+ /** Sub-agent kind (internal), explore agents route to economy models. */
96
+ subagent?: "explore" | "general";
97
+ /**
98
+ * Reuse an existing session id instead of generating one. Sub-agents pass the
99
+ * parent's so their spend rolls into the same session; hosts can pass a stored
100
+ * id to continue a session across process restarts.
101
+ */
102
+ sessionId?: string;
103
+ /** Set false to disable automatic compaction (`/compact` still works). */
104
+ autoCompact?: boolean;
105
+ /** Context window override when the catalog has no entry for the model. */
106
+ contextTokens?: number;
107
+ /** Model used to summarise history. Defaults to a cheap long-context one. */
108
+ compactionModel?: string;
109
+ /**
110
+ * I/O delegation (`delegate.ts`): bulk reads and boilerplate run on a worker
111
+ * model so their payload never enters this session's context. On by default.
112
+ * Turning it off also disarms the read tool's size gate, since a refusal with
113
+ * nowhere to redirect to would just be a broken read.
114
+ */
115
+ delegation?: {
116
+ enabled?: boolean;
117
+ /** Worker model. Defaults to the cheapest model the plan allows. */
118
+ model?: string;
119
+ /** Whole-file reads above this many lines are refused. */
120
+ minLines?: number;
121
+ };
122
+ }
123
+ /** What delegation kept out of the context, for hosts that want to show it. */
124
+ export interface DelegationTotals {
125
+ calls: number;
126
+ /** Tokens of file corpus that never entered the conversation. */
127
+ contextTokensSaved: number;
128
+ /** What the worker model actually cost. Delegation is cheaper, not free. */
129
+ workerUsage: UsageTotals;
130
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,13 @@
1
+ /** Project config at <cwd>/.9p/config.json */
2
+ export interface ProjectConfig {
3
+ /** Path to the Obsidian-style vault (absolute, or relative to the project). */
4
+ vault?: string;
5
+ }
6
+ export declare function loadProjectConfig(cwd: string): ProjectConfig;
7
+ export declare function saveProjectConfig(cwd: string, cfg: ProjectConfig): void;
8
+ /** Absolute vault path if configured and existing, else null. */
9
+ export declare function resolveVault(cwd: string): string | null;
10
+ /** Create the vault structure. Returns the files created (existing files untouched). */
11
+ export declare function scaffoldVault(vaultPath: string): string[];
12
+ /** Standing rules injected into the system prompt when a vault is configured. */
13
+ export declare function vaultProtocol(vaultPath: string): string;
package/dist/vault.js ADDED
@@ -0,0 +1,81 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export function loadProjectConfig(cwd) {
4
+ try {
5
+ return JSON.parse(fs.readFileSync(path.join(cwd, ".9p", "config.json"), "utf8"));
6
+ }
7
+ catch {
8
+ return {};
9
+ }
10
+ }
11
+ export function saveProjectConfig(cwd, cfg) {
12
+ const dir = path.join(cwd, ".9p");
13
+ fs.mkdirSync(dir, { recursive: true });
14
+ fs.writeFileSync(path.join(dir, "config.json"), JSON.stringify(cfg, null, 2) + "\n");
15
+ }
16
+ /** Absolute vault path if configured and existing, else null. */
17
+ export function resolveVault(cwd) {
18
+ const cfg = loadProjectConfig(cwd);
19
+ if (!cfg.vault)
20
+ return null;
21
+ const abs = path.resolve(cwd, cfg.vault);
22
+ return fs.existsSync(abs) ? abs : null;
23
+ }
24
+ const INDEX_MD = `# Vault index
25
+
26
+ Master map of this vault. Every note should be reachable from here via [[wiki links]].
27
+
28
+ ## Sections
29
+ - [[jobs/README|Jobs]], reusable procedures the agent follows step by step
30
+ - [[decisions/README|Decisions]], what was decided and why
31
+ - daily/, one note per working day; the newest note is the current state of work
32
+ - projects/. One folder per project; each has a master note named after its folder
33
+ - graph/, generated codebase maps (run \`9p map\` to create or refresh)
34
+ `;
35
+ const JOBS_README = `# Jobs
36
+
37
+ Reusable procedures ("do X" playbooks). One note per job, named after the job.
38
+ The note lists numbered steps and [[wiki links]] to every note the job depends on -
39
+ reading the job note should give the agent everything it needs to do the job.
40
+ `;
41
+ const DECISIONS_README = `# Decisions
42
+
43
+ One note per significant decision: what was decided, why, alternatives rejected,
44
+ and [[wiki links]] to affected notes/files. Newest decisions at the top of each note.
45
+ `;
46
+ const DAILY_README = `# Daily notes
47
+
48
+ One note per working day, named YYYY-MM-DD.md. Format: a short index of the day's
49
+ entries at the top, details below. Written by the agent at the end of significant work -
50
+ this is how sessions (and multiple agents) stay in sync.
51
+ `;
52
+ /** Create the vault structure. Returns the files created (existing files untouched). */
53
+ export function scaffoldVault(vaultPath) {
54
+ const created = [];
55
+ const writeIfMissing = (rel, content) => {
56
+ const file = path.join(vaultPath, rel);
57
+ if (fs.existsSync(file))
58
+ return;
59
+ fs.mkdirSync(path.dirname(file), { recursive: true });
60
+ fs.writeFileSync(file, content);
61
+ created.push(file);
62
+ };
63
+ fs.mkdirSync(path.join(vaultPath, "projects"), { recursive: true });
64
+ fs.mkdirSync(path.join(vaultPath, "graph"), { recursive: true });
65
+ writeIfMissing("INDEX.md", INDEX_MD);
66
+ writeIfMissing("jobs/README.md", JOBS_README);
67
+ writeIfMissing("decisions/README.md", DECISIONS_README);
68
+ writeIfMissing("daily/README.md", DAILY_README);
69
+ return created;
70
+ }
71
+ /** Standing rules injected into the system prompt when a vault is configured. */
72
+ export function vaultProtocol(vaultPath) {
73
+ return `Vault memory protocol. This project keeps long-term memory in an Obsidian-style vault at: ${vaultPath}
74
+ - The vault is your memory. For unfamiliar tasks, first read ${vaultPath}/INDEX.md and follow the relevant [[wiki links]] instead of re-exploring from scratch.
75
+ - Append to existing notes rather than creating new ones, unless a genuinely new topic demands its own note. Bloat is the enemy.
76
+ - Connect notes with [[wiki links]] and keep INDEX.md pointing at anything new.
77
+ - After completing significant work, log it in ${vaultPath}/daily/YYYY-MM-DD.md using today's date from your context (never guess the date): short index at the top of the note, details below.
78
+ - Record important decisions and their reasons in ${vaultPath}/decisions/.
79
+ - If ${vaultPath}/graph/ contains codebase maps, consult them before broadly re-reading raw files.`;
80
+ }
81
+ //# sourceMappingURL=vault.js.map
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@9thprotocol/agent-core",
3
+ "version": "0.1.0",
4
+ "description": "9th Protocol agent engine: loop, tools, permissions, sub-agents, context management",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": ["dist", "LICENSE", "README.md"],
10
+ "publishConfig": { "access": "public" },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/Samso9th/9th-protocol-packages"
14
+ },
15
+ "engines": {
16
+ "node": ">=22.0.0"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.11.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.10.5",
27
+ "typescript": "^5.7.3"
28
+ }
29
+ }