@alvin0/ai-agent-sdk-instructions-node 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 alvin0 (chaulamdinhai) <chaulamdinhai@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # @alvin0/ai-agent-sdk-instructions-node
2
+
3
+ Node filesystem discovery for `AGENTS.md`-style project instructions, delivered
4
+ as a core **context section**.
5
+
6
+ Runtime: **Node 22.12+**.
7
+
8
+ ```bash
9
+ pnpm add @alvin0/ai-agent-sdk-core @alvin0/ai-agent-sdk-instructions-node
10
+ ```
11
+
12
+ The core SDK never reads a file. It exposes `ContextSection`: a callback the
13
+ turn loop re-runs before every model round, owning exactly one node on the model
14
+ surface and rewriting it only when its revision changes. This package is the
15
+ Node implementation of that callback for project instruction files.
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { defineAgent } from '@alvin0/ai-agent-sdk-core'
21
+ import { createProjectInstructionsSection } from '@alvin0/ai-agent-sdk-instructions-node'
22
+
23
+ const agent = defineAgent({
24
+ id: 'coder',
25
+ instructions: 'You are a coding agent.',
26
+ contextSections: [createProjectInstructionsSection({ cwd: process.cwd() })],
27
+ })
28
+ ```
29
+
30
+ Mount it per session instead when the working directory belongs to the session
31
+ rather than the definition:
32
+
33
+ ```ts
34
+ const session = runtime.agent(agent).createSession({
35
+ contextSections: [createProjectInstructionsSection({ cwd: workspaceDir })],
36
+ })
37
+ ```
38
+
39
+ ## Discovery
40
+
41
+ 1. The configured `globalFile`, when one is supplied. There is no default —
42
+ a library does not guess where a host keeps a user's standing instructions.
43
+ 2. Walk up from `cwd` until a `projectRootMarkers` entry is found (`.git` by
44
+ default), then read the candidates from that root **down to `cwd`**,
45
+ inclusive. The walk never passes the root.
46
+ 3. Once a tool call touches a file below `cwd`, that subtree's directories join
47
+ the scan and stay in scope for the rest of the session.
48
+
49
+ Within one directory, `perDirectory: 'first'` (the default) takes the first
50
+ present candidate — `AGENTS.override.md`, then `AGENTS.md` — and `'all'` takes
51
+ every present candidate. Files with identical trimmed content collapse to the
52
+ first occurrence, so a symlinked or copied twin is not rendered twice.
53
+
54
+ Rendering is broad-to-specific, each file introduced by
55
+ `Instructions from: <path relative to the project root>`. Content is admitted
56
+ whole under `maxBytes` (64 KiB by default); anything that does not fit is named
57
+ in a closing line rather than silently cut.
58
+
59
+ ## Sharing one instance across agents
60
+
61
+ One section object is normally mounted on a definition that many sessions
62
+ instantiate — every member of a team, every worker cloned from a lead — and
63
+ those sessions run concurrently. Everything this section accumulates is keyed by
64
+ the conversation scope the loop passes to `resolve`, so a team member that reads
65
+ into `packages/api` does not put that directory's instructions in front of its
66
+ peers. Buckets are bounded by `maxTrackedScopes` and the least recently used one
67
+ is dropped first. A bare `runTurn` with no trace identity shares one unscoped
68
+ bucket.
69
+
70
+ Mounting the section on the session instead of the definition is still the right
71
+ call when the working directory belongs to the session — the scope keying makes
72
+ sharing *safe*, not preferable.
73
+
74
+ A skill-relative path never counts as a workspace path: arguments carrying a
75
+ `skillId` (as `read_skill_resource({ skillId, path })` does) are ignored, so a
76
+ skill resource named `references/patterns.md` cannot pull `references/AGENTS.md`
77
+ into context.
78
+
79
+ ## Why a section and not a skill
80
+
81
+ Project instructions are always-on: a model that never loads them has violated
82
+ the project's conventions without knowing they existed. Skills are the opposite
83
+ contract — advertised by description and loaded only when the model picks them.
84
+
85
+ Why a section and not `additionalInstructions`: the system prompt is the cache
86
+ prefix, and these files change during a session (the cwd moves, a tool reaches
87
+ into a new subtree, someone edits the file). Rewriting the prefix each time
88
+ would discard the prompt cache, and appending has no way to retract text that no
89
+ longer applies. A section writes into the conversation surface, replaces its own
90
+ node in place, and costs nothing on the steps where nothing changed.
91
+
92
+ ## Options
93
+
94
+ | Option | Default | Meaning |
95
+ |---|---|---|
96
+ | `id` | `project-instructions` | Section id on the model surface |
97
+ | `cwd` | `process.cwd()` | Session working directory |
98
+ | `globalFile` | — | Absolute path read before any project file |
99
+ | `projectRootMarkers` | `['.git']` | Entries that stop the upward walk |
100
+ | `fileNames` | `['AGENTS.override.md', 'AGENTS.md']` | Same-directory candidates, in precedence order |
101
+ | `perDirectory` | `'first'` | `first` or `all` present candidates per directory |
102
+ | `maxBytes` | `65536` | Total UTF-8 ceiling for the rendered section |
103
+ | `maxFileBytes` | `maxBytes` | Per-file UTF-8 ceiling |
104
+ | `nested` | `true` | Scan subtrees a tool call reaches into |
105
+ | `maxNestedDirs` | `256` | Most subtree directories kept in scope at once |
106
+ | `onNestedLimit` | — | Called once per conversation when that cap is reached |
107
+ | `maxTrackedScopes` | `64` | Conversations whose subtrees this instance remembers |
108
+ | `filePathFromTouch` | reads `file_path`/`path`/`filePath` | Which committed call touched which path |
109
+ | `intro` | see `DEFAULT_INTRO` | Paragraph placed above the files |
110
+ | `retractionText` | see `DEFAULT_RETRACTION` | Written when every file leaves scope |
111
+
112
+ A failed tool call never contributes a path: a read that errored did not enter
113
+ that directory.
114
+
115
+ Every retained directory is re-probed before every model round, so `maxNestedDirs`
116
+ bounds the per-step filesystem cost of an agent that walks a large tree. Once the
117
+ cap is reached the directories already in scope win, and `onNestedLimit` fires
118
+ once so a host can log it. `maxFileBytes` is bounded by `maxBytes`: a file larger
119
+ than the whole section is skipped without being read.
@@ -0,0 +1,214 @@
1
+ import { ContextSection, ContextSectionScope } from "@alvin0/ai-agent-sdk-core";
2
+ //#region src/config.d.ts
3
+ /**
4
+ * Normalized discovery configuration for filesystem instruction files.
5
+ *
6
+ * @module @alvin0/ai-agent-sdk-instructions-node/config
7
+ */
8
+ declare const DEFAULT_FILE_NAMES: readonly string[];
9
+ declare const DEFAULT_PROJECT_ROOT_MARKERS: readonly string[];
10
+ declare const DEFAULT_MAX_BYTES = 65536;
11
+ declare const DEFAULT_SECTION_ID = "project-instructions";
12
+ /** Descendant directories retained for re-probing before every model round. */
13
+ declare const DEFAULT_MAX_NESTED_DIRS = 256;
14
+ /** Conversations whose nested scope one section instance remembers at once. */
15
+ declare const DEFAULT_MAX_TRACKED_SCOPES = 64;
16
+ /** One tool call the loop committed, as seen by the path extractor. */
17
+ interface InstructionToolTouch {
18
+ readonly toolName: string;
19
+ readonly rawArguments: string;
20
+ readonly failed: boolean;
21
+ }
22
+ interface ProjectInstructionsOptions {
23
+ /** Section id on the model surface. Defaults to `project-instructions`. */
24
+ readonly id?: string;
25
+ /** Session working directory. Defaults to `process.cwd()`. */
26
+ readonly cwd?: string;
27
+ /**
28
+ * One absolute file read before any project file — the user's own standing
29
+ * instructions. No default: a package does not guess where a host keeps them.
30
+ */
31
+ readonly globalFile?: string;
32
+ /** Directory entries that stop the upward walk. Defaults to `['.git']`. */
33
+ readonly projectRootMarkers?: readonly string[];
34
+ /** Same-directory candidates in precedence order. Defaults to override-then-`AGENTS.md`. */
35
+ readonly fileNames?: readonly string[];
36
+ /**
37
+ * `first` loads the first candidate present in a directory (Codex semantics);
38
+ * `all` loads every present candidate (deepseek-harness semantics).
39
+ * Defaults to `first`.
40
+ */
41
+ readonly perDirectory?: 'first' | 'all';
42
+ /** Total UTF-8 ceiling for the rendered section. Defaults to 64 KiB. */
43
+ readonly maxBytes?: number;
44
+ /** Per-file UTF-8 ceiling. Defaults to {@link ProjectInstructionsOptions.maxBytes}. */
45
+ readonly maxFileBytes?: number;
46
+ /**
47
+ * Also scan directories below `cwd` once a tool touches a file inside them.
48
+ *
49
+ * A model that opens `packages/api/handler.ts` gets `packages/api/AGENTS.md`
50
+ * without the host having predicted the path. Defaults to true.
51
+ */
52
+ readonly nested?: boolean;
53
+ /**
54
+ * Which committed tool call touched which path.
55
+ *
56
+ * Defaults to reading a `file_path` or `path` string from JSON arguments of a
57
+ * call that succeeded. Replace it when your tools name the argument otherwise.
58
+ */
59
+ readonly filePathFromTouch?: (touch: InstructionToolTouch) => string | undefined;
60
+ /**
61
+ * Most descendant directories kept in scope at once. Defaults to 256.
62
+ *
63
+ * Each retained directory is re-probed before every model round, so this
64
+ * bounds the per-step filesystem cost of an agent that walks a large tree.
65
+ */
66
+ readonly maxNestedDirs?: number;
67
+ /** Called once per conversation when {@link ProjectInstructionsOptions.maxNestedDirs} is reached. */
68
+ readonly onNestedLimit?: (limit: number) => void;
69
+ /**
70
+ * Conversations whose accumulated subtrees this section instance remembers.
71
+ *
72
+ * One section object is normally mounted on a definition that many sessions
73
+ * instantiate, so its per-conversation state is kept in a bounded map and the
74
+ * least recently used conversation is dropped first. Defaults to 64.
75
+ */
76
+ readonly maxTrackedScopes?: number;
77
+ /** Leading paragraph placed above the files. A default is supplied. */
78
+ readonly intro?: string;
79
+ /** Replaces the live node when every instruction file disappears. */
80
+ readonly retractionText?: string;
81
+ }
82
+ interface ResolvedInstructionsConfig {
83
+ readonly id: string;
84
+ readonly cwd: string;
85
+ readonly globalFile: string | undefined;
86
+ readonly projectRootMarkers: readonly string[];
87
+ readonly fileNames: readonly string[];
88
+ readonly perDirectory: 'first' | 'all';
89
+ readonly maxBytes: number;
90
+ readonly maxFileBytes: number;
91
+ readonly nested: boolean;
92
+ readonly maxNestedDirs: number;
93
+ readonly onNestedLimit: ((limit: number) => void) | undefined;
94
+ readonly maxTrackedScopes: number;
95
+ readonly filePathFromTouch: (touch: InstructionToolTouch) => string | undefined;
96
+ readonly intro: string;
97
+ readonly retractionText: string;
98
+ }
99
+ declare const DEFAULT_INTRO: string;
100
+ declare const DEFAULT_RETRACTION: string;
101
+ /**
102
+ * Read a filesystem path out of one committed tool call.
103
+ *
104
+ * Only successful calls are considered: a failed read did not enter a
105
+ * directory, so it must not pull that directory's instructions into context.
106
+ * @param touch - the committed call.
107
+ * @returns the touched path, when the arguments carry a recognizable one.
108
+ */
109
+ declare function defaultFilePathFromTouch(touch: InstructionToolTouch): string | undefined;
110
+ //#endregion
111
+ //#region src/section.d.ts
112
+ interface ProjectInstructionsSection extends ContextSection {
113
+ /**
114
+ * Absolute paths rendered for one conversation, for host diagnostics.
115
+ * @param scope - the conversation to report on; defaults to the unscoped one.
116
+ * @returns the paths in the order they were rendered.
117
+ */
118
+ loadedPaths(scope?: ContextSectionScope): readonly string[];
119
+ }
120
+ /**
121
+ * Build the AGENTS.md-compatible context section.
122
+ *
123
+ * Discovery walks up from `cwd` to the project root, reads the configured
124
+ * candidates from the root down, and — once a tool touches a file below `cwd` —
125
+ * adds that subtree's instruction files too. Content is rendered into one
126
+ * surface node; the loop rewrites it only when the digest changes.
127
+ *
128
+ * One instance is safe to mount on a definition that many sessions instantiate.
129
+ * Everything it accumulates is keyed by the conversation scope the loop hands
130
+ * to `resolve`, so a team member that reads into `packages/api` does not put
131
+ * that directory's instructions in front of its peers. Sessions with no trace
132
+ * identity — a bare `runTurn` — share one unscoped bucket.
133
+ *
134
+ * ```ts
135
+ * const agent = defineAgent({
136
+ * id: 'coder',
137
+ * instructions: 'You are a coding agent.',
138
+ * contextSections: [createProjectInstructionsSection({ cwd: process.cwd() })],
139
+ * })
140
+ * ```
141
+ * @param options - discovery, budget, and rendering controls.
142
+ * @returns a section ready to mount on an agent or a session.
143
+ */
144
+ declare function createProjectInstructionsSection(options?: ProjectInstructionsOptions): ProjectInstructionsSection;
145
+ //#endregion
146
+ //#region src/discovery.d.ts
147
+ /** One instruction file that exists and was read. */
148
+ interface LoadedInstructionFile {
149
+ readonly absolutePath: string;
150
+ /** Project-root-relative path shown to the model. */
151
+ readonly displayPath: string;
152
+ readonly content: string;
153
+ readonly mtimeMs: number;
154
+ readonly size: number;
155
+ }
156
+ /**
157
+ * Walk upward until a root marker is found.
158
+ * @param cwd - absolute starting directory.
159
+ * @param markers - directory entries that identify a root.
160
+ * @returns the marked ancestor, or `cwd` when no marker exists above it.
161
+ */
162
+ declare function findProjectRoot(cwd: string, markers: readonly string[], signal?: AbortSignal): Promise<string>;
163
+ /**
164
+ * Directories from the project root down to `cwd`, inclusive.
165
+ * @param root - the project root.
166
+ * @param cwd - the session working directory.
167
+ * @returns root-first directory chain.
168
+ */
169
+ declare function ancestorChain(root: string, cwd: string): string[];
170
+ /**
171
+ * Directories crossed between `base` and a touched file, excluding `base`.
172
+ * @param base - the directory the chain already covers.
173
+ * @param touchedPath - absolute path, or one relative to `base`.
174
+ * @returns shallowest-first descendant directories, empty when the path escapes `base`.
175
+ */
176
+ declare function descendantDirsBetween(base: string, touchedPath: string): string[];
177
+ /**
178
+ * Order directories shallowest-first, so a deeper file still reads as the more
179
+ * specific one. Path length is not depth: `/a/bbbb` is shallower than `/a/b/c`.
180
+ * @param left - first directory.
181
+ * @param right - second directory.
182
+ * @returns a comparator result usable with `Array#sort`.
183
+ */
184
+ declare function byDepthThenPath(left: string, right: string): number;
185
+ //#endregion
186
+ //#region src/render.d.ts
187
+ interface RenderedInstructions {
188
+ readonly text: string;
189
+ readonly revision: string;
190
+ /** Files that made it into the rendered text, in surface order. */
191
+ readonly included: readonly LoadedInstructionFile[];
192
+ /** Files dropped because the section ran out of budget. */
193
+ readonly omitted: readonly LoadedInstructionFile[];
194
+ }
195
+ /**
196
+ * Drop files whose trimmed content already appeared earlier.
197
+ * @param files - discovered files in precedence order.
198
+ * @returns the first occurrence of each distinct content.
199
+ */
200
+ declare function dedupeByContent(files: readonly LoadedInstructionFile[]): LoadedInstructionFile[];
201
+ /**
202
+ * Compose the model-facing text under the total byte ceiling.
203
+ *
204
+ * Files are admitted whole, nearest-first is *not* used: precedence order is
205
+ * broad-to-specific, and a specific file dropped for budget is reported rather
206
+ * than silently cut in half.
207
+ * @param files - deduplicated files in precedence order.
208
+ * @param config - normalized configuration.
209
+ * @returns the rendered section, or undefined when nothing is in scope.
210
+ */
211
+ declare function renderInstructions(files: readonly LoadedInstructionFile[], config: ResolvedInstructionsConfig): RenderedInstructions | undefined;
212
+ //#endregion
213
+ export { DEFAULT_FILE_NAMES, DEFAULT_INTRO, DEFAULT_MAX_BYTES, DEFAULT_MAX_NESTED_DIRS, DEFAULT_MAX_TRACKED_SCOPES, DEFAULT_PROJECT_ROOT_MARKERS, DEFAULT_RETRACTION, DEFAULT_SECTION_ID, type InstructionToolTouch, type LoadedInstructionFile, type ProjectInstructionsOptions, type ProjectInstructionsSection, type RenderedInstructions, ancestorChain, byDepthThenPath, createProjectInstructionsSection, dedupeByContent, defaultFilePathFromTouch, descendantDirsBetween, findProjectRoot, renderInstructions };
214
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/config.ts","../src/section.ts","../src/discovery.ts","../src/render.ts"],"mappings":";;;;;;;cAMa;cACA;cACA;cACA;;cAEA;;cAEA;;UAKI;WACN;WACA;WACA;;UAGM;;WAEN;;WAEA;;;;;WAKA;;WAEA;;WAEA;;;;;;WAMA;;WAEA;;WAEA;;;;;;;WAOA;;;;;;;WAOA,qBAAqB,OAAO;;;;;;;WAO5B;;WAEA,iBAAiB;;;;;;;;WAQjB;;WAEA;;WAEA;;UAGM;WACN;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA,iBAAiB;WACjB;WACA,oBAAoB,OAAO;WAC3B;WACA;;cAGE;cAIA;;;;;;;;;iBAWG,yBAAyB,OAAO;;;UCnE/B,mCAAmC;;;;;;EAMlD,YAAY,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsCN,iCACd,UAAS,6BACR;;;;UCrFc;WACN;;WAEA;WACA;WACA;WACA;;;;;;;;iBA8BW,gBACpB,aACA,4BACA,SAAS,cACR;;;;;;;iBAkBa,cAAc,cAAc;;;;;;;iBAoB5B,sBAAsB,cAAc;;;;;;;;iBAiGpC,gBAAgB,cAAc;;;UCjL7B;WACN;WACA;;WAEA,mBAAmB;;WAEnB,kBAAkB;;;;;;;iBAqBb,gBAAgB,gBAAgB,0BAA0B;;;;;;;;;;;iBAyB1D,mBACd,gBAAgB,yBAChB,QAAQ,6BACP"}
package/dist/index.mjs ADDED
@@ -0,0 +1,437 @@
1
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
2
+ import { defineContextSection } from "@alvin0/ai-agent-sdk-core";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { createHash } from "node:crypto";
5
+
6
+ //#region src/config.ts
7
+ /**
8
+ * Normalized discovery configuration for filesystem instruction files.
9
+ *
10
+ * @module @alvin0/ai-agent-sdk-instructions-node/config
11
+ */
12
+ const DEFAULT_FILE_NAMES = Object.freeze(["AGENTS.override.md", "AGENTS.md"]);
13
+ const DEFAULT_PROJECT_ROOT_MARKERS = Object.freeze([".git"]);
14
+ const DEFAULT_MAX_BYTES = 65536;
15
+ const DEFAULT_SECTION_ID = "project-instructions";
16
+ /** Descendant directories retained for re-probing before every model round. */
17
+ const DEFAULT_MAX_NESTED_DIRS = 256;
18
+ /** Conversations whose nested scope one section instance remembers at once. */
19
+ const DEFAULT_MAX_TRACKED_SCOPES = 64;
20
+ const RESERVED_SEGMENTS = /* @__PURE__ */ new Set([
21
+ "",
22
+ ".",
23
+ ".."
24
+ ]);
25
+ const DEFAULT_INTRO = "The following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.";
26
+ const DEFAULT_RETRACTION = "The workspace instructions provided earlier no longer apply. No instruction files are currently in scope.";
27
+ /**
28
+ * Read a filesystem path out of one committed tool call.
29
+ *
30
+ * Only successful calls are considered: a failed read did not enter a
31
+ * directory, so it must not pull that directory's instructions into context.
32
+ * @param touch - the committed call.
33
+ * @returns the touched path, when the arguments carry a recognizable one.
34
+ */
35
+ function defaultFilePathFromTouch(touch) {
36
+ if (touch.failed) return void 0;
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(touch.rawArguments);
40
+ } catch {
41
+ return;
42
+ }
43
+ if (typeof parsed !== "object" || parsed === null) return void 0;
44
+ const record = parsed;
45
+ if (typeof record.skillId === "string") return void 0;
46
+ for (const key of [
47
+ "file_path",
48
+ "filePath",
49
+ "path"
50
+ ]) {
51
+ const value = record[key];
52
+ if (typeof value === "string" && value.trim().length > 0) return value.trim();
53
+ }
54
+ }
55
+ function positiveBytes(value, fallback, name) {
56
+ if (value === void 0) return fallback;
57
+ if (!Number.isFinite(value) || value <= 0) throw new TypeError(`${name} must be a positive finite number`);
58
+ return Math.floor(value);
59
+ }
60
+ /**
61
+ * Apply defaults and drop candidates that are not plain file names.
62
+ * @param options - user-facing options.
63
+ * @param cwd - fallback working directory when none was supplied.
64
+ * @returns the normalized configuration.
65
+ */
66
+ function resolveInstructionsConfig(options, cwd) {
67
+ const maxBytes = positiveBytes(options.maxBytes, DEFAULT_MAX_BYTES, "maxBytes");
68
+ const fileNames = (options.fileNames ?? DEFAULT_FILE_NAMES).filter((name) => !RESERVED_SEGMENTS.has(name) && !/[\\/]/.test(name));
69
+ if (fileNames.length === 0) throw new TypeError("fileNames must contain at least one plain file name");
70
+ return Object.freeze({
71
+ id: options.id ?? "project-instructions",
72
+ cwd: options.cwd ?? cwd,
73
+ globalFile: options.globalFile,
74
+ projectRootMarkers: Object.freeze([...options.projectRootMarkers ?? DEFAULT_PROJECT_ROOT_MARKERS]),
75
+ fileNames: Object.freeze(fileNames),
76
+ perDirectory: options.perDirectory ?? "first",
77
+ maxBytes,
78
+ maxFileBytes: positiveBytes(options.maxFileBytes, maxBytes, "maxFileBytes"),
79
+ nested: options.nested ?? true,
80
+ maxNestedDirs: positiveBytes(options.maxNestedDirs, 256, "maxNestedDirs"),
81
+ onNestedLimit: options.onNestedLimit,
82
+ maxTrackedScopes: positiveBytes(options.maxTrackedScopes, 64, "maxTrackedScopes"),
83
+ filePathFromTouch: options.filePathFromTouch ?? defaultFilePathFromTouch,
84
+ intro: options.intro ?? "The following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.",
85
+ retractionText: options.retractionText ?? "The workspace instructions provided earlier no longer apply. No instruction files are currently in scope."
86
+ });
87
+ }
88
+
89
+ //#endregion
90
+ //#region src/discovery.ts
91
+ /**
92
+ * Filesystem walk for instruction files: project root, ancestor chain, and the
93
+ * descendant directories a tool call reached into.
94
+ *
95
+ * @module @alvin0/ai-agent-sdk-instructions-node/discovery
96
+ */
97
+ async function isFile(path) {
98
+ try {
99
+ const info = await stat(path);
100
+ return info.isFile() ? {
101
+ mtimeMs: info.mtimeMs,
102
+ size: info.size
103
+ } : void 0;
104
+ } catch {
105
+ return;
106
+ }
107
+ }
108
+ async function hasMarker(dir, markers) {
109
+ for (const marker of markers) try {
110
+ await stat(join(dir, marker));
111
+ return true;
112
+ } catch {}
113
+ return false;
114
+ }
115
+ /**
116
+ * Walk upward until a root marker is found.
117
+ * @param cwd - absolute starting directory.
118
+ * @param markers - directory entries that identify a root.
119
+ * @returns the marked ancestor, or `cwd` when no marker exists above it.
120
+ */
121
+ async function findProjectRoot(cwd, markers, signal) {
122
+ if (markers.length === 0) return resolve(cwd);
123
+ let current = resolve(cwd);
124
+ while (true) {
125
+ signal?.throwIfAborted();
126
+ if (await hasMarker(current, markers)) return current;
127
+ const parent = dirname(current);
128
+ if (parent === current) return resolve(cwd);
129
+ current = parent;
130
+ }
131
+ }
132
+ /**
133
+ * Directories from the project root down to `cwd`, inclusive.
134
+ * @param root - the project root.
135
+ * @param cwd - the session working directory.
136
+ * @returns root-first directory chain.
137
+ */
138
+ function ancestorChain(root, cwd) {
139
+ const resolvedRoot = resolve(root);
140
+ const chain = [];
141
+ let current = resolve(cwd);
142
+ while (current !== resolvedRoot) {
143
+ chain.push(current);
144
+ const parent = dirname(current);
145
+ if (parent === current) return [resolvedRoot];
146
+ current = parent;
147
+ }
148
+ chain.push(resolvedRoot);
149
+ return chain.reverse();
150
+ }
151
+ /**
152
+ * Directories crossed between `base` and a touched file, excluding `base`.
153
+ * @param base - the directory the chain already covers.
154
+ * @param touchedPath - absolute path, or one relative to `base`.
155
+ * @returns shallowest-first descendant directories, empty when the path escapes `base`.
156
+ */
157
+ function descendantDirsBetween(base, touchedPath) {
158
+ const resolvedBase = resolve(base);
159
+ const target = isAbsolute(touchedPath) ? resolve(touchedPath) : resolve(resolvedBase, touchedPath);
160
+ const dir = dirname(target);
161
+ const rel = relative(resolvedBase, dir);
162
+ if (rel.length === 0 || rel.startsWith("..") || isAbsolute(rel)) return [];
163
+ const segments = rel.split(sep).filter((segment) => segment.length > 0);
164
+ return segments.map((_, index) => join(resolvedBase, ...segments.slice(0, index + 1)));
165
+ }
166
+ function utf8Bytes$1(value) {
167
+ return Buffer.byteLength(value, "utf8");
168
+ }
169
+ /**
170
+ * Read the instruction candidates present in one directory.
171
+ * @param dir - absolute directory to probe.
172
+ * @param root - display base.
173
+ * @param config - normalized configuration.
174
+ * @returns the files that exist, in candidate precedence order.
175
+ */
176
+ async function directoryInstructionFiles(dir, root, config, signal) {
177
+ const found = [];
178
+ for (const name of config.fileNames) {
179
+ signal?.throwIfAborted();
180
+ const absolutePath = join(dir, name);
181
+ const info = await isFile(absolutePath);
182
+ if (info === void 0) continue;
183
+ const loaded = await readInstructionFile(absolutePath, root, config, info);
184
+ if (loaded !== void 0) {
185
+ found.push(loaded);
186
+ if (config.perDirectory === "first") break;
187
+ }
188
+ }
189
+ return found;
190
+ }
191
+ /**
192
+ * Read one instruction file, skipping it when it exceeds the per-file ceiling.
193
+ * @param absolutePath - the file to read.
194
+ * @param root - display base.
195
+ * @param config - normalized configuration.
196
+ * @param info - stat data already collected for the file.
197
+ * @returns the loaded file, or undefined when it is empty, oversized, or unreadable.
198
+ */
199
+ async function readInstructionFile(absolutePath, root, config, info) {
200
+ const ceiling = Math.min(config.maxFileBytes, config.maxBytes);
201
+ if (info.size > ceiling) return void 0;
202
+ let content;
203
+ try {
204
+ content = await readFile(absolutePath, "utf8");
205
+ } catch {
206
+ return;
207
+ }
208
+ if (content.trim().length === 0) return void 0;
209
+ if (utf8Bytes$1(content) > ceiling) return void 0;
210
+ return {
211
+ absolutePath,
212
+ displayPath: absolutePath.startsWith(resolve(root) + sep) ? relative(resolve(root), absolutePath) : absolutePath,
213
+ content,
214
+ mtimeMs: info.mtimeMs,
215
+ size: info.size
216
+ };
217
+ }
218
+ /**
219
+ * Probe the configured global file, when one was supplied.
220
+ * @param config - normalized configuration.
221
+ * @returns the loaded global file, or undefined.
222
+ */
223
+ async function globalInstructionFile(config) {
224
+ if (config.globalFile === void 0) return void 0;
225
+ const absolutePath = resolve(config.globalFile);
226
+ const info = await isFile(absolutePath);
227
+ if (info === void 0) return void 0;
228
+ return readInstructionFile(absolutePath, dirname(absolutePath), config, info);
229
+ }
230
+ /**
231
+ * Order directories shallowest-first, so a deeper file still reads as the more
232
+ * specific one. Path length is not depth: `/a/bbbb` is shallower than `/a/b/c`.
233
+ * @param left - first directory.
234
+ * @param right - second directory.
235
+ * @returns a comparator result usable with `Array#sort`.
236
+ */
237
+ function byDepthThenPath(left, right) {
238
+ const depth = left.split(sep).length - right.split(sep).length;
239
+ return depth !== 0 ? depth : left < right ? -1 : left > right ? 1 : 0;
240
+ }
241
+
242
+ //#endregion
243
+ //#region src/render.ts
244
+ /**
245
+ * Rendering and byte accounting for the instruction section.
246
+ *
247
+ * @module @alvin0/ai-agent-sdk-instructions-node/render
248
+ */
249
+ function utf8Bytes(value) {
250
+ return Buffer.byteLength(value, "utf8");
251
+ }
252
+ /** Whitespace-insensitive identity, so a symlinked or copied twin collapses. */
253
+ function trimmedDigest(content) {
254
+ return createHash("sha256").update(content.trim()).digest("hex");
255
+ }
256
+ function sectionText(file) {
257
+ return `Instructions from: ${file.displayPath}\n\n${file.content.trim()}`;
258
+ }
259
+ /**
260
+ * Drop files whose trimmed content already appeared earlier.
261
+ * @param files - discovered files in precedence order.
262
+ * @returns the first occurrence of each distinct content.
263
+ */
264
+ function dedupeByContent(files) {
265
+ const seenPaths = /* @__PURE__ */ new Set();
266
+ const seenContent = /* @__PURE__ */ new Set();
267
+ const kept = [];
268
+ for (const file of files) {
269
+ if (seenPaths.has(file.absolutePath)) continue;
270
+ seenPaths.add(file.absolutePath);
271
+ const digest = trimmedDigest(file.content);
272
+ if (seenContent.has(digest)) continue;
273
+ seenContent.add(digest);
274
+ kept.push(file);
275
+ }
276
+ return kept;
277
+ }
278
+ /**
279
+ * Compose the model-facing text under the total byte ceiling.
280
+ *
281
+ * Files are admitted whole, nearest-first is *not* used: precedence order is
282
+ * broad-to-specific, and a specific file dropped for budget is reported rather
283
+ * than silently cut in half.
284
+ * @param files - deduplicated files in precedence order.
285
+ * @param config - normalized configuration.
286
+ * @returns the rendered section, or undefined when nothing is in scope.
287
+ */
288
+ function renderInstructions(files, config) {
289
+ if (files.length === 0) return void 0;
290
+ const included = [];
291
+ const omitted = [];
292
+ let used = utf8Bytes(config.intro);
293
+ for (const file of files) {
294
+ const cost = utf8Bytes(sectionText(file)) + 2;
295
+ if (used + cost > config.maxBytes) {
296
+ omitted.push(file);
297
+ continue;
298
+ }
299
+ used += cost;
300
+ included.push(file);
301
+ }
302
+ if (included.length === 0) return void 0;
303
+ const notice = omitted.length === 0 ? "" : `\n\nOmitted for the ${config.maxBytes}-byte instruction budget: ${omitted.map((file) => file.displayPath).join(", ")}.`;
304
+ const text = `${config.intro}\n\n${included.map(sectionText).join("\n\n")}${notice}`;
305
+ return {
306
+ text,
307
+ revision: createHash("sha256").update(text).digest("hex"),
308
+ included: Object.freeze(included),
309
+ omitted: Object.freeze(omitted)
310
+ };
311
+ }
312
+
313
+ //#endregion
314
+ //#region src/section.ts
315
+ /**
316
+ * The filesystem-backed {@link ContextSection} itself.
317
+ *
318
+ * @module @alvin0/ai-agent-sdk-instructions-node/section
319
+ */
320
+ /**
321
+ * Discover every instruction file currently in scope.
322
+ * @param config - normalized configuration.
323
+ * @param extraDirs - descendant directories a tool call reached into.
324
+ * @returns files in broad-to-specific precedence order.
325
+ */
326
+ async function discover(config, extraDirs, signal) {
327
+ const cwd = resolve(config.cwd);
328
+ const projectRoot = await findProjectRoot(cwd, config.projectRootMarkers, signal);
329
+ const files = [];
330
+ const global = await globalInstructionFile(config);
331
+ if (global !== void 0) files.push(global);
332
+ const seen = new Set(ancestorChain(projectRoot, cwd));
333
+ const dirs = [...seen];
334
+ for (const dir of [...extraDirs].sort(byDepthThenPath)) {
335
+ if (seen.has(dir)) continue;
336
+ seen.add(dir);
337
+ dirs.push(dir);
338
+ }
339
+ for (const dir of dirs) files.push(...await directoryInstructionFiles(dir, projectRoot, config, signal));
340
+ return {
341
+ files,
342
+ projectRoot
343
+ };
344
+ }
345
+ function scopeKey(scope) {
346
+ return `${scope?.agentId ?? ""}\u0000${scope?.conversationId ?? ""}`;
347
+ }
348
+ /**
349
+ * Build the AGENTS.md-compatible context section.
350
+ *
351
+ * Discovery walks up from `cwd` to the project root, reads the configured
352
+ * candidates from the root down, and — once a tool touches a file below `cwd` —
353
+ * adds that subtree's instruction files too. Content is rendered into one
354
+ * surface node; the loop rewrites it only when the digest changes.
355
+ *
356
+ * One instance is safe to mount on a definition that many sessions instantiate.
357
+ * Everything it accumulates is keyed by the conversation scope the loop hands
358
+ * to `resolve`, so a team member that reads into `packages/api` does not put
359
+ * that directory's instructions in front of its peers. Sessions with no trace
360
+ * identity — a bare `runTurn` — share one unscoped bucket.
361
+ *
362
+ * ```ts
363
+ * const agent = defineAgent({
364
+ * id: 'coder',
365
+ * instructions: 'You are a coding agent.',
366
+ * contextSections: [createProjectInstructionsSection({ cwd: process.cwd() })],
367
+ * })
368
+ * ```
369
+ * @param options - discovery, budget, and rendering controls.
370
+ * @returns a section ready to mount on an agent or a session.
371
+ */
372
+ function createProjectInstructionsSection(options = {}) {
373
+ const config = resolveInstructionsConfig(options, process.cwd());
374
+ const scopes = /* @__PURE__ */ new Map();
375
+ const stateFor = (scope) => {
376
+ const key = scopeKey(scope);
377
+ const existing = scopes.get(key);
378
+ if (existing !== void 0) {
379
+ scopes.delete(key);
380
+ scopes.set(key, existing);
381
+ return existing;
382
+ }
383
+ const created = {
384
+ nestedDirs: /* @__PURE__ */ new Set(),
385
+ nestedLimitReported: false,
386
+ loaded: Object.freeze([])
387
+ };
388
+ scopes.set(key, created);
389
+ while (scopes.size > config.maxTrackedScopes) {
390
+ const oldest = scopes.keys().next();
391
+ if (oldest.done === true) break;
392
+ scopes.delete(oldest.value);
393
+ }
394
+ return created;
395
+ };
396
+ const section = defineContextSection({
397
+ id: config.id,
398
+ retractionText: config.retractionText,
399
+ async resolve(input) {
400
+ const state = stateFor(input.scope);
401
+ if (config.nested) for (const touch of input.touches) {
402
+ const path = config.filePathFromTouch(touch);
403
+ if (path === void 0) continue;
404
+ for (const dir of descendantDirsBetween(config.cwd, path)) {
405
+ if (state.nestedDirs.size >= config.maxNestedDirs) {
406
+ if (!state.nestedLimitReported) {
407
+ state.nestedLimitReported = true;
408
+ config.onNestedLimit?.(config.maxNestedDirs);
409
+ }
410
+ break;
411
+ }
412
+ state.nestedDirs.add(dir);
413
+ }
414
+ }
415
+ input.signal.throwIfAborted();
416
+ const { files } = await discover(config, [...state.nestedDirs], input.signal);
417
+ const rendered = renderInstructions(dedupeByContent(files), config);
418
+ if (rendered === void 0) {
419
+ state.loaded = Object.freeze([]);
420
+ return;
421
+ }
422
+ state.loaded = Object.freeze(rendered.included.map((file) => file.absolutePath));
423
+ return {
424
+ revision: rendered.revision,
425
+ text: rendered.text
426
+ };
427
+ }
428
+ });
429
+ return Object.freeze({
430
+ ...section,
431
+ loadedPaths: (scope) => scopes.get(scopeKey(scope))?.loaded ?? Object.freeze([])
432
+ });
433
+ }
434
+
435
+ //#endregion
436
+ export { DEFAULT_FILE_NAMES, DEFAULT_INTRO, DEFAULT_MAX_BYTES, DEFAULT_MAX_NESTED_DIRS, DEFAULT_MAX_TRACKED_SCOPES, DEFAULT_PROJECT_ROOT_MARKERS, DEFAULT_RETRACTION, DEFAULT_SECTION_ID, ancestorChain, byDepthThenPath, createProjectInstructionsSection, dedupeByContent, defaultFilePathFromTouch, descendantDirsBetween, findProjectRoot, renderInstructions };
437
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["utf8Bytes"],"sources":["../src/config.ts","../src/discovery.ts","../src/render.ts","../src/section.ts"],"sourcesContent":["/**\n * Normalized discovery configuration for filesystem instruction files.\n *\n * @module @alvin0/ai-agent-sdk-instructions-node/config\n */\n\nexport const DEFAULT_FILE_NAMES = Object.freeze(['AGENTS.override.md', 'AGENTS.md'])\nexport const DEFAULT_PROJECT_ROOT_MARKERS = Object.freeze(['.git'])\nexport const DEFAULT_MAX_BYTES = 65_536\nexport const DEFAULT_SECTION_ID = 'project-instructions'\n/** Descendant directories retained for re-probing before every model round. */\nexport const DEFAULT_MAX_NESTED_DIRS = 256\n/** Conversations whose nested scope one section instance remembers at once. */\nexport const DEFAULT_MAX_TRACKED_SCOPES = 64\n\nconst RESERVED_SEGMENTS = new Set(['', '.', '..'])\n\n/** One tool call the loop committed, as seen by the path extractor. */\nexport interface InstructionToolTouch {\n readonly toolName: string\n readonly rawArguments: string\n readonly failed: boolean\n}\n\nexport interface ProjectInstructionsOptions {\n /** Section id on the model surface. Defaults to `project-instructions`. */\n readonly id?: string\n /** Session working directory. Defaults to `process.cwd()`. */\n readonly cwd?: string\n /**\n * One absolute file read before any project file — the user's own standing\n * instructions. No default: a package does not guess where a host keeps them.\n */\n readonly globalFile?: string\n /** Directory entries that stop the upward walk. Defaults to `['.git']`. */\n readonly projectRootMarkers?: readonly string[]\n /** Same-directory candidates in precedence order. Defaults to override-then-`AGENTS.md`. */\n readonly fileNames?: readonly string[]\n /**\n * `first` loads the first candidate present in a directory (Codex semantics);\n * `all` loads every present candidate (deepseek-harness semantics).\n * Defaults to `first`.\n */\n readonly perDirectory?: 'first' | 'all'\n /** Total UTF-8 ceiling for the rendered section. Defaults to 64 KiB. */\n readonly maxBytes?: number\n /** Per-file UTF-8 ceiling. Defaults to {@link ProjectInstructionsOptions.maxBytes}. */\n readonly maxFileBytes?: number\n /**\n * Also scan directories below `cwd` once a tool touches a file inside them.\n *\n * A model that opens `packages/api/handler.ts` gets `packages/api/AGENTS.md`\n * without the host having predicted the path. Defaults to true.\n */\n readonly nested?: boolean\n /**\n * Which committed tool call touched which path.\n *\n * Defaults to reading a `file_path` or `path` string from JSON arguments of a\n * call that succeeded. Replace it when your tools name the argument otherwise.\n */\n readonly filePathFromTouch?: (touch: InstructionToolTouch) => string | undefined\n /**\n * Most descendant directories kept in scope at once. Defaults to 256.\n *\n * Each retained directory is re-probed before every model round, so this\n * bounds the per-step filesystem cost of an agent that walks a large tree.\n */\n readonly maxNestedDirs?: number\n /** Called once per conversation when {@link ProjectInstructionsOptions.maxNestedDirs} is reached. */\n readonly onNestedLimit?: (limit: number) => void\n /**\n * Conversations whose accumulated subtrees this section instance remembers.\n *\n * One section object is normally mounted on a definition that many sessions\n * instantiate, so its per-conversation state is kept in a bounded map and the\n * least recently used conversation is dropped first. Defaults to 64.\n */\n readonly maxTrackedScopes?: number\n /** Leading paragraph placed above the files. A default is supplied. */\n readonly intro?: string\n /** Replaces the live node when every instruction file disappears. */\n readonly retractionText?: string\n}\n\nexport interface ResolvedInstructionsConfig {\n readonly id: string\n readonly cwd: string\n readonly globalFile: string | undefined\n readonly projectRootMarkers: readonly string[]\n readonly fileNames: readonly string[]\n readonly perDirectory: 'first' | 'all'\n readonly maxBytes: number\n readonly maxFileBytes: number\n readonly nested: boolean\n readonly maxNestedDirs: number\n readonly onNestedLimit: ((limit: number) => void) | undefined\n readonly maxTrackedScopes: number\n readonly filePathFromTouch: (touch: InstructionToolTouch) => string | undefined\n readonly intro: string\n readonly retractionText: string\n}\n\nexport const DEFAULT_INTRO = 'The following workspace instructions may be relevant to your work. '\n + 'Use them as guidance when applicable. More specific instructions take precedence over broader '\n + 'ones. They do not override system, developer, or direct user instructions.'\n\nexport const DEFAULT_RETRACTION = 'The workspace instructions provided earlier no longer apply. '\n + 'No instruction files are currently in scope.'\n\n/**\n * Read a filesystem path out of one committed tool call.\n *\n * Only successful calls are considered: a failed read did not enter a\n * directory, so it must not pull that directory's instructions into context.\n * @param touch - the committed call.\n * @returns the touched path, when the arguments carry a recognizable one.\n */\nexport function defaultFilePathFromTouch(touch: InstructionToolTouch): string | undefined {\n if (touch.failed) return undefined\n let parsed: unknown\n try {\n parsed = JSON.parse(touch.rawArguments)\n } catch {\n return undefined\n }\n if (typeof parsed !== 'object' || parsed === null) return undefined\n const record = parsed as Record<string, unknown>\n // A `path` beside a `skillId` addresses a resource inside that skill's\n // bundle, not a place in the workspace. `read_skill_resource({skillId, path})`\n // would otherwise resolve `references/patterns.md` against the cwd and pull a\n // wholly unrelated directory's instructions into context.\n if (typeof record.skillId === 'string') return undefined\n for (const key of ['file_path', 'filePath', 'path']) {\n const value = record[key]\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n }\n return undefined\n}\n\nfunction positiveBytes(value: number | undefined, fallback: number, name: string): number {\n if (value === undefined) return fallback\n if (!Number.isFinite(value) || value <= 0) {\n throw new TypeError(`${name} must be a positive finite number`)\n }\n return Math.floor(value)\n}\n\n/**\n * Apply defaults and drop candidates that are not plain file names.\n * @param options - user-facing options.\n * @param cwd - fallback working directory when none was supplied.\n * @returns the normalized configuration.\n */\nexport function resolveInstructionsConfig(\n options: ProjectInstructionsOptions,\n cwd: string,\n): ResolvedInstructionsConfig {\n const maxBytes = positiveBytes(options.maxBytes, DEFAULT_MAX_BYTES, 'maxBytes')\n const fileNames = (options.fileNames ?? DEFAULT_FILE_NAMES).filter(name => (\n !RESERVED_SEGMENTS.has(name) && !/[\\\\/]/.test(name)\n ))\n if (fileNames.length === 0) throw new TypeError('fileNames must contain at least one plain file name')\n return Object.freeze({\n id: options.id ?? DEFAULT_SECTION_ID,\n cwd: options.cwd ?? cwd,\n globalFile: options.globalFile,\n projectRootMarkers: Object.freeze([...options.projectRootMarkers ?? DEFAULT_PROJECT_ROOT_MARKERS]),\n fileNames: Object.freeze(fileNames),\n perDirectory: options.perDirectory ?? 'first',\n maxBytes,\n maxFileBytes: positiveBytes(options.maxFileBytes, maxBytes, 'maxFileBytes'),\n nested: options.nested ?? true,\n maxNestedDirs: positiveBytes(options.maxNestedDirs, DEFAULT_MAX_NESTED_DIRS, 'maxNestedDirs'),\n onNestedLimit: options.onNestedLimit,\n maxTrackedScopes: positiveBytes(\n options.maxTrackedScopes, DEFAULT_MAX_TRACKED_SCOPES, 'maxTrackedScopes',\n ),\n filePathFromTouch: options.filePathFromTouch ?? defaultFilePathFromTouch,\n intro: options.intro ?? DEFAULT_INTRO,\n retractionText: options.retractionText ?? DEFAULT_RETRACTION,\n })\n}\n","/**\n * Filesystem walk for instruction files: project root, ancestor chain, and the\n * descendant directories a tool call reached into.\n *\n * @module @alvin0/ai-agent-sdk-instructions-node/discovery\n */\n\nimport { readFile, stat } from 'node:fs/promises'\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport type { ResolvedInstructionsConfig } from './config.ts'\n\n/** One instruction file that exists and was read. */\nexport interface LoadedInstructionFile {\n readonly absolutePath: string\n /** Project-root-relative path shown to the model. */\n readonly displayPath: string\n readonly content: string\n readonly mtimeMs: number\n readonly size: number\n}\n\nasync function isFile(path: string): Promise<{ mtimeMs: number; size: number } | undefined> {\n try {\n const info = await stat(path)\n return info.isFile() ? { mtimeMs: info.mtimeMs, size: info.size } : undefined\n } catch {\n // A missing candidate is the normal case; an unreadable one is skipped for\n // the same reason — assembled context must never fail a turn.\n return undefined\n }\n}\n\nasync function hasMarker(dir: string, markers: readonly string[]): Promise<boolean> {\n for (const marker of markers) {\n try {\n await stat(join(dir, marker))\n return true\n } catch { /* keep looking */ }\n }\n return false\n}\n\n/**\n * Walk upward until a root marker is found.\n * @param cwd - absolute starting directory.\n * @param markers - directory entries that identify a root.\n * @returns the marked ancestor, or `cwd` when no marker exists above it.\n */\nexport async function findProjectRoot(\n cwd: string,\n markers: readonly string[],\n signal?: AbortSignal,\n): Promise<string> {\n if (markers.length === 0) return resolve(cwd)\n let current = resolve(cwd)\n while (true) {\n signal?.throwIfAborted()\n if (await hasMarker(current, markers)) return current\n const parent = dirname(current)\n if (parent === current) return resolve(cwd)\n current = parent\n }\n}\n\n/**\n * Directories from the project root down to `cwd`, inclusive.\n * @param root - the project root.\n * @param cwd - the session working directory.\n * @returns root-first directory chain.\n */\nexport function ancestorChain(root: string, cwd: string): string[] {\n const resolvedRoot = resolve(root)\n const chain: string[] = []\n let current = resolve(cwd)\n while (current !== resolvedRoot) {\n chain.push(current)\n const parent = dirname(current)\n if (parent === current) return [resolvedRoot]\n current = parent\n }\n chain.push(resolvedRoot)\n return chain.reverse()\n}\n\n/**\n * Directories crossed between `base` and a touched file, excluding `base`.\n * @param base - the directory the chain already covers.\n * @param touchedPath - absolute path, or one relative to `base`.\n * @returns shallowest-first descendant directories, empty when the path escapes `base`.\n */\nexport function descendantDirsBetween(base: string, touchedPath: string): string[] {\n const resolvedBase = resolve(base)\n const target = isAbsolute(touchedPath) ? resolve(touchedPath) : resolve(resolvedBase, touchedPath)\n const dir = dirname(target)\n const rel = relative(resolvedBase, dir)\n if (rel.length === 0 || rel.startsWith('..') || isAbsolute(rel)) return []\n const segments = rel.split(sep).filter(segment => segment.length > 0)\n return segments.map((_, index) => join(resolvedBase, ...segments.slice(0, index + 1)))\n}\n\nfunction utf8Bytes(value: string): number {\n return Buffer.byteLength(value, 'utf8')\n}\n\n/**\n * Read the instruction candidates present in one directory.\n * @param dir - absolute directory to probe.\n * @param root - display base.\n * @param config - normalized configuration.\n * @returns the files that exist, in candidate precedence order.\n */\nexport async function directoryInstructionFiles(\n dir: string,\n root: string,\n config: ResolvedInstructionsConfig,\n signal?: AbortSignal,\n): Promise<LoadedInstructionFile[]> {\n const found: LoadedInstructionFile[] = []\n for (const name of config.fileNames) {\n signal?.throwIfAborted()\n const absolutePath = join(dir, name)\n const info = await isFile(absolutePath)\n if (info === undefined) continue\n const loaded = await readInstructionFile(absolutePath, root, config, info)\n if (loaded !== undefined) {\n found.push(loaded)\n if (config.perDirectory === 'first') break\n }\n }\n return found\n}\n\n/**\n * Read one instruction file, skipping it when it exceeds the per-file ceiling.\n * @param absolutePath - the file to read.\n * @param root - display base.\n * @param config - normalized configuration.\n * @param info - stat data already collected for the file.\n * @returns the loaded file, or undefined when it is empty, oversized, or unreadable.\n */\nexport async function readInstructionFile(\n absolutePath: string,\n root: string,\n config: ResolvedInstructionsConfig,\n info: { mtimeMs: number; size: number },\n): Promise<LoadedInstructionFile | undefined> {\n // A file larger than the whole section can never be rendered usefully, and\n // reading it first would spend the memory to prove that. The section ceiling\n // bounds the per-file ceiling for exactly that reason.\n const ceiling = Math.min(config.maxFileBytes, config.maxBytes)\n if (info.size > ceiling) return undefined\n let content: string\n try {\n content = await readFile(absolutePath, 'utf8')\n } catch {\n return undefined\n }\n if (content.trim().length === 0) return undefined\n if (utf8Bytes(content) > ceiling) return undefined\n const displayPath = absolutePath.startsWith(resolve(root) + sep)\n ? relative(resolve(root), absolutePath)\n : absolutePath\n return { absolutePath, displayPath, content, mtimeMs: info.mtimeMs, size: info.size }\n}\n\n/**\n * Probe the configured global file, when one was supplied.\n * @param config - normalized configuration.\n * @returns the loaded global file, or undefined.\n */\nexport async function globalInstructionFile(\n config: ResolvedInstructionsConfig,\n): Promise<LoadedInstructionFile | undefined> {\n if (config.globalFile === undefined) return undefined\n const absolutePath = resolve(config.globalFile)\n const info = await isFile(absolutePath)\n if (info === undefined) return undefined\n return readInstructionFile(absolutePath, dirname(absolutePath), config, info)\n}\n\n/**\n * Order directories shallowest-first, so a deeper file still reads as the more\n * specific one. Path length is not depth: `/a/bbbb` is shallower than `/a/b/c`.\n * @param left - first directory.\n * @param right - second directory.\n * @returns a comparator result usable with `Array#sort`.\n */\nexport function byDepthThenPath(left: string, right: string): number {\n const depth = left.split(sep).length - right.split(sep).length\n return depth !== 0 ? depth : (left < right ? -1 : left > right ? 1 : 0)\n}\n","/**\n * Rendering and byte accounting for the instruction section.\n *\n * @module @alvin0/ai-agent-sdk-instructions-node/render\n */\n\nimport { createHash } from 'node:crypto'\nimport type { ResolvedInstructionsConfig } from './config.ts'\nimport type { LoadedInstructionFile } from './discovery.ts'\n\nexport interface RenderedInstructions {\n readonly text: string\n readonly revision: string\n /** Files that made it into the rendered text, in surface order. */\n readonly included: readonly LoadedInstructionFile[]\n /** Files dropped because the section ran out of budget. */\n readonly omitted: readonly LoadedInstructionFile[]\n}\n\nfunction utf8Bytes(value: string): number {\n return Buffer.byteLength(value, 'utf8')\n}\n\n/** Whitespace-insensitive identity, so a symlinked or copied twin collapses. */\nfunction trimmedDigest(content: string): string {\n return createHash('sha256').update(content.trim()).digest('hex')\n}\n\nfunction sectionText(file: LoadedInstructionFile): string {\n return `Instructions from: ${file.displayPath}\\n\\n${file.content.trim()}`\n}\n\n/**\n * Drop files whose trimmed content already appeared earlier.\n * @param files - discovered files in precedence order.\n * @returns the first occurrence of each distinct content.\n */\nexport function dedupeByContent(files: readonly LoadedInstructionFile[]): LoadedInstructionFile[] {\n const seenPaths = new Set<string>()\n const seenContent = new Set<string>()\n const kept: LoadedInstructionFile[] = []\n for (const file of files) {\n if (seenPaths.has(file.absolutePath)) continue\n seenPaths.add(file.absolutePath)\n const digest = trimmedDigest(file.content)\n if (seenContent.has(digest)) continue\n seenContent.add(digest)\n kept.push(file)\n }\n return kept\n}\n\n/**\n * Compose the model-facing text under the total byte ceiling.\n *\n * Files are admitted whole, nearest-first is *not* used: precedence order is\n * broad-to-specific, and a specific file dropped for budget is reported rather\n * than silently cut in half.\n * @param files - deduplicated files in precedence order.\n * @param config - normalized configuration.\n * @returns the rendered section, or undefined when nothing is in scope.\n */\nexport function renderInstructions(\n files: readonly LoadedInstructionFile[],\n config: ResolvedInstructionsConfig,\n): RenderedInstructions | undefined {\n if (files.length === 0) return undefined\n const included: LoadedInstructionFile[] = []\n const omitted: LoadedInstructionFile[] = []\n let used = utf8Bytes(config.intro)\n for (const file of files) {\n const block = sectionText(file)\n const cost = utf8Bytes(block) + 2\n if (used + cost > config.maxBytes) {\n omitted.push(file)\n continue\n }\n used += cost\n included.push(file)\n }\n if (included.length === 0) return undefined\n const notice = omitted.length === 0\n ? ''\n : `\\n\\nOmitted for the ${config.maxBytes}-byte instruction budget: `\n + `${omitted.map(file => file.displayPath).join(', ')}.`\n const text = `${config.intro}\\n\\n${included.map(sectionText).join('\\n\\n')}${notice}`\n // The digest covers exactly what the model reads, so any change to content,\n // ordering, or the omission notice produces a new revision and one rewrite.\n return {\n text,\n revision: createHash('sha256').update(text).digest('hex'),\n included: Object.freeze(included),\n omitted: Object.freeze(omitted),\n }\n}\n","/**\n * The filesystem-backed {@link ContextSection} itself.\n *\n * @module @alvin0/ai-agent-sdk-instructions-node/section\n */\n\nimport { resolve } from 'node:path'\nimport { defineContextSection } from '@alvin0/ai-agent-sdk-core'\nimport type { ContextSection, ContextSectionScope, ContextSectionState } from '@alvin0/ai-agent-sdk-core'\nimport {\n resolveInstructionsConfig,\n type ProjectInstructionsOptions,\n type ResolvedInstructionsConfig,\n} from './config.ts'\nimport {\n ancestorChain, byDepthThenPath, descendantDirsBetween, directoryInstructionFiles, findProjectRoot,\n globalInstructionFile, type LoadedInstructionFile,\n} from './discovery.ts'\nimport { dedupeByContent, renderInstructions } from './render.ts'\n\n/**\n * Discover every instruction file currently in scope.\n * @param config - normalized configuration.\n * @param extraDirs - descendant directories a tool call reached into.\n * @returns files in broad-to-specific precedence order.\n */\nasync function discover(\n config: ResolvedInstructionsConfig,\n extraDirs: readonly string[],\n signal: AbortSignal,\n): Promise<{ files: LoadedInstructionFile[]; projectRoot: string }> {\n const cwd = resolve(config.cwd)\n const projectRoot = await findProjectRoot(cwd, config.projectRootMarkers, signal)\n const files: LoadedInstructionFile[] = []\n const global = await globalInstructionFile(config)\n if (global !== undefined) files.push(global)\n const seen = new Set(ancestorChain(projectRoot, cwd))\n const dirs = [...seen]\n // Nested directories come after the chain and shallowest-first, so a deeper\n // file still reads as the more specific one.\n for (const dir of [...extraDirs].sort(byDepthThenPath)) {\n if (seen.has(dir)) continue\n seen.add(dir)\n dirs.push(dir)\n }\n for (const dir of dirs) {\n files.push(...await directoryInstructionFiles(dir, projectRoot, config, signal))\n }\n return { files, projectRoot }\n}\n\nexport interface ProjectInstructionsSection extends ContextSection {\n /**\n * Absolute paths rendered for one conversation, for host diagnostics.\n * @param scope - the conversation to report on; defaults to the unscoped one.\n * @returns the paths in the order they were rendered.\n */\n loadedPaths(scope?: ContextSectionScope): readonly string[]\n}\n\n/** Mutable state one conversation accumulates. */\ninterface ScopeState {\n readonly nestedDirs: Set<string>\n nestedLimitReported: boolean\n loaded: readonly string[]\n}\n\nfunction scopeKey(scope: ContextSectionScope | undefined): string {\n return `${scope?.agentId ?? ''}\\u0000${scope?.conversationId ?? ''}`\n}\n\n/**\n * Build the AGENTS.md-compatible context section.\n *\n * Discovery walks up from `cwd` to the project root, reads the configured\n * candidates from the root down, and — once a tool touches a file below `cwd` —\n * adds that subtree's instruction files too. Content is rendered into one\n * surface node; the loop rewrites it only when the digest changes.\n *\n * One instance is safe to mount on a definition that many sessions instantiate.\n * Everything it accumulates is keyed by the conversation scope the loop hands\n * to `resolve`, so a team member that reads into `packages/api` does not put\n * that directory's instructions in front of its peers. Sessions with no trace\n * identity — a bare `runTurn` — share one unscoped bucket.\n *\n * ```ts\n * const agent = defineAgent({\n * id: 'coder',\n * instructions: 'You are a coding agent.',\n * contextSections: [createProjectInstructionsSection({ cwd: process.cwd() })],\n * })\n * ```\n * @param options - discovery, budget, and rendering controls.\n * @returns a section ready to mount on an agent or a session.\n */\nexport function createProjectInstructionsSection(\n options: ProjectInstructionsOptions = {},\n): ProjectInstructionsSection {\n const config = resolveInstructionsConfig(options, process.cwd())\n // Insertion-ordered, so the first key is the least recently created bucket.\n const scopes = new Map<string, ScopeState>()\n\n const stateFor = (scope: ContextSectionScope | undefined): ScopeState => {\n const key = scopeKey(scope)\n const existing = scopes.get(key)\n if (existing !== undefined) {\n // Refresh recency so an active conversation is never the one evicted.\n scopes.delete(key)\n scopes.set(key, existing)\n return existing\n }\n const created: ScopeState = {\n nestedDirs: new Set<string>(), nestedLimitReported: false, loaded: Object.freeze([]),\n }\n scopes.set(key, created)\n while (scopes.size > config.maxTrackedScopes) {\n const oldest = scopes.keys().next()\n if (oldest.done === true) break\n scopes.delete(oldest.value)\n }\n return created\n }\n\n const section = defineContextSection({\n id: config.id,\n retractionText: config.retractionText,\n async resolve(input): Promise<ContextSectionState | undefined> {\n const state = stateFor(input.scope)\n if (config.nested) {\n for (const touch of input.touches) {\n const path = config.filePathFromTouch(touch)\n if (path === undefined) continue\n for (const dir of descendantDirsBetween(config.cwd, path)) {\n // Every retained directory is re-probed on every model round. An\n // agent that walks a large tree would otherwise turn one section\n // into thousands of stat calls per step, so the set is capped and\n // the directories already in scope win.\n if (state.nestedDirs.size >= config.maxNestedDirs) {\n if (!state.nestedLimitReported) {\n state.nestedLimitReported = true\n config.onNestedLimit?.(config.maxNestedDirs)\n }\n break\n }\n state.nestedDirs.add(dir)\n }\n }\n }\n input.signal.throwIfAborted()\n const { files } = await discover(config, [...state.nestedDirs], input.signal)\n const rendered = renderInstructions(dedupeByContent(files), config)\n if (rendered === undefined) {\n state.loaded = Object.freeze([])\n return undefined\n }\n state.loaded = Object.freeze(rendered.included.map(file => file.absolutePath))\n return { revision: rendered.revision, text: rendered.text }\n },\n })\n\n return Object.freeze({\n ...section,\n loadedPaths: (scope?: ContextSectionScope) => scopes.get(scopeKey(scope))?.loaded ?? Object.freeze([]),\n })\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@alvin0/ai-agent-sdk-instructions-node",
3
+ "author": {
4
+ "name": "alvin0 - chaulamdinhai",
5
+ "email": "chaulamdinhai@gmail.com"
6
+ },
7
+ "version": "0.1.0",
8
+ "description": "Node filesystem-backed AGENTS.md context section for ai-agent-sdk",
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/alvin0/ai-agent-sdk.git",
13
+ "directory": "packages/instructions-node"
14
+ },
15
+ "homepage": "https://github.com/alvin0/ai-agent-sdk/tree/main/packages/instructions-node#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/alvin0/ai-agent-sdk/issues"
18
+ },
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "main": "./dist/index.mjs",
27
+ "types": "./dist/index.d.mts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.mts",
31
+ "import": "./dist/index.mjs",
32
+ "default": "./dist/index.mjs"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "provenance": true
39
+ },
40
+ "peerDependencies": {
41
+ "@alvin0/ai-agent-sdk-core": "^0.1.0"
42
+ },
43
+ "devDependencies": {
44
+ "@alvin0/ai-agent-sdk-core": "^0.1.0",
45
+ "@arethetypeswrong/cli": "0.18.5",
46
+ "@types/node": "26.4.0",
47
+ "publint": "0.3.24",
48
+ "tsdown": "0.22.14",
49
+ "typescript": "7.0.2",
50
+ "vitest": "4.1.11"
51
+ },
52
+ "engines": {
53
+ "node": ">=22.12"
54
+ },
55
+ "aiAgentSdk": {
56
+ "runtime": "node",
57
+ "coreApi": 1,
58
+ "roles": [
59
+ "context-section"
60
+ ]
61
+ },
62
+ "scripts": {
63
+ "build": "tsdown",
64
+ "clean": "node -e \"for(const p of ['dist','artifacts'])require('node:fs').rmSync(p,{recursive:true,force:true})\"",
65
+ "typecheck": "tsc --noEmit",
66
+ "test": "vitest run --config vitest.config.ts",
67
+ "pack": "pnpm pack --pack-destination artifacts",
68
+ "test:pack": "node ../../scripts/test-packed-node-capability.mts instructions-node",
69
+ "check:publint": "publint",
70
+ "check:types": "attw --profile esm-only --pack ."
71
+ }
72
+ }