@hachej/boring-agent 0.4.4 → 0.4.5

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/README.md CHANGED
@@ -305,6 +305,8 @@ Each evidence entry must link to a completed native ToolTask, its original assis
305
305
 
306
306
  `createSkillsExtension` tells the model that a user message starting with `/<skill-name>` is an explicit request to load that skill first, which is what the chat's `/` menu inserts. `defineAgent(...).skills` lists each skill's name and description for such a menu.
307
307
 
308
+ With self-evolution, workspace skills may be flat `.agent/skills/<name>.md` files or `.agent/skills/<name>/SKILL.md` folders. Only `SKILL.md` defines a skill inside a folder; other Markdown and deeper files are companions, not skills. `load_skill` returns the body followed by “This skill's folder is `.agent/skills/<name>`; paths in it are relative to that folder.” (For flat files the folder is `.agent/skills`.) A skill can say `see portions.md` or `see tables/2026.csv`; the agent reads that path under the returned folder with Pi's native `read`, through the same workspace provider. Scanning and loading do not read the companions. No special companion folder, new file tool or additional access is granted. Host-given `Skill` data stays `{ name, description, body }`: with no source folder, `load_skill` returns its body unchanged.
309
+
308
310
  ## Sub-agents and schedules
309
311
 
310
312
  These follow [Pi Durable on Cloudflare](https://earendil.com/posts/pi-durable-on-cloudflare/) and stay host-agnostic: a Node process and a Durable Object mount the same tools.
@@ -323,7 +325,7 @@ These follow [Pi Durable on Cloudflare](https://earendil.com/posts/pi-durable-on
323
325
 
324
326
  `defineAgent({ …, selfEvolving: true, workspace })` lets the agent keep its own standing instructions, skills and tools in the `.agent/` folder of its workspace and apply them with a `reload` tool; the plan and its reasons are in [SELF-EVOLUTION.md](../../docs/architecture/SELF-EVOLUTION.md). Off by default.
325
327
 
326
- - One native extension per workspace, named `self-evolving:<workspace>`, so two workspaces in one host never replace each other's tools. Without a fixed `workspace` the agent works in a workspace per conversation: `inWorkspace(name)` returns the agent of workspace `name` (made once, then the same), whose configuration selects only `self-evolving:<name>`; the host creates each conversation with the agent of its owner's workspace and installs and reloads that workspace's extension when the workspace opens, so one harness and one registry never offer one workspace's agent-written tools to another's. It carries the skills (the host's, then `.agent/skills/*.md` parsed with `parseSkill`, behind the usual `load_skill`), `reload`, the agent-written tools and the `agent-written` prompt section. It is selected last; the host's `instructions` become the first section (`host-instructions`) of `agent.<id>`, because Pi renders native `instructions` after every extension section.
328
+ - One native extension per workspace, named `self-evolving:<workspace>`, so two workspaces in one host never replace each other's tools. Without a fixed `workspace` the agent works in a workspace per conversation: `inWorkspace(name)` returns the agent of workspace `name` (made once, then the same), whose configuration selects only `self-evolving:<name>`; the host creates each conversation with the agent of its owner's workspace and installs and reloads that workspace's extension when the workspace opens, so one harness and one registry never offer one workspace's agent-written tools to another's. It carries the skills (the host's, then `.agent/skills/*.md` and `.agent/skills/<name>/SKILL.md` parsed with `parseSkill`, behind the usual `load_skill`), `reload`, the agent-written tools and the `agent-written` prompt section. It is selected last; the host's `instructions` become the first section (`host-instructions`) of `agent.<id>`, because Pi renders native `instructions` after every extension section.
327
329
  - `.agent/AGENTS.md` is rendered by that section on every request through the request's environment, labelled as agent-written and capped at `MAX_AGENT_INSTRUCTIONS` (16,000) characters.
328
330
  - `.agent/tools/<name>.json` is `{ name, description, parameters, run }`: `name` snake_case and not one of the agent's other tools, `parameters` a JSON Schema with `"type": "object"`, `run` one command line; a path under `.agent/` that `run` names must exist. Each becomes a native `ToolRegistration` (through `jsonSchemaTool`, so Pi refuses arguments that do not match `parameters` before `run` starts) whose `execute` runs `run` through the call's own `ExecutionEnv.exec` (the same provider instance as the files it was read from), with the arguments as one line of JSON on standard input (a quoted here-document); the output is the result and a non-zero exit is an error.
329
331
  - `reload` (and `DefinedAgent.reload(env, context)`, which a host calls on open and for the person's `/reload`) reads `.agent/` through the given environment's `FileSystem`, rebuilds the extension and calls the native `registry.install()` in every registry `install` was given (same name: replaced in place). Reloads are serialised. The report (`SelfEvolutionReport`, its `text` is the tool result) names tools added, changed and removed, the agent's skills, the instructions and every load error (invalid JSON, schema, missing script, name conflict, unreadable file). A reload never imports or evaluates anything. Without `exec` the tools are reported unavailable and skipped; instructions and skills still load.
@@ -1,5 +1,6 @@
1
1
  import { defineExtension, defineTool } from '@earendil-works/pi-durable';
2
2
  import { Type } from '@earendil-works/pi-ai';
3
+ import { skillInFolder } from './skills.js';
3
4
  import { jsonSchemaTool } from './json-schema-tool.js';
4
5
  /*
5
6
  * Self-evolution (docs/architecture/SELF-EVOLUTION.md, laws SELF-1..4 in this package's README): an agent keeps its own standing
@@ -102,6 +103,27 @@ async function filesIn(env, directory, suffix, context) {
102
103
  return listed.error.code === 'not_found' ? 'missing' : { error: listed.error.message };
103
104
  return listed.value.filter(entry => entry.kind === 'file' && entry.name.endsWith(suffix)).map(entry => `${directory}/${entry.name}`).sort();
104
105
  }
106
+ /** Flat Markdown skills and `SKILL.md` in immediate child folders; companion files are not definitions. */
107
+ async function skillFilesIn(env, context, errors) {
108
+ const listed = await env.listDir(SKILLS, context);
109
+ if (!listed.ok) {
110
+ if (listed.error.code !== 'not_found')
111
+ errors.push({ path: SKILLS, message: listed.error.message });
112
+ return [];
113
+ }
114
+ const paths = listed.value.filter(entry => entry.kind === 'file' && entry.name.endsWith('.md')).map(entry => `${SKILLS}/${entry.name}`);
115
+ for (const entry of listed.value.filter(entry => entry.kind === 'directory')) {
116
+ const folder = `${SKILLS}/${entry.name}`;
117
+ const contents = await env.listDir(folder, context);
118
+ if (!contents.ok) {
119
+ errors.push({ path: folder, message: contents.error.message });
120
+ continue;
121
+ }
122
+ if (contents.value.some(file => file.kind === 'file' && file.name === 'SKILL.md'))
123
+ paths.push(`${folder}/SKILL.md`);
124
+ }
125
+ return paths.sort();
126
+ }
105
127
  export function createSelfEvolution(options) {
106
128
  const { name, parseSkill, skillsExtension } = options;
107
129
  const hostSkills = [...options.skills];
@@ -113,7 +135,7 @@ export function createSelfEvolution(options) {
113
135
  name: 'reload',
114
136
  description: `Apply the changes you made to your own ${AGENT_FOLDER}/ folder in the workspace, then return what was added, changed and removed and every load error. `
115
137
  + `${INSTRUCTIONS}: standing instructions for yourself, shown after the host's instructions on every request. `
116
- + `${SKILLS}/<name>.md: skills (front matter with name and description between --- lines, then the instructions), loaded with load_skill. `
138
+ + `${SKILLS}/<name>.md or ${SKILLS}/<name>/SKILL.md: skills (front matter with name and description between --- lines, then the instructions), loaded with load_skill; paths in a skill are relative to its folder and read with read. `
117
139
  + `${TOOLS}/<name>.json: your own tools, {"name": "snake_case", "description": "...", "parameters": {a JSON Schema with "type": "object"}, "run": "a command run in the workspace, for example sh ${TOOLS}/<name>.sh"}; `
118
140
  + 'the arguments arrive as one line of JSON on standard input and the command output is the result. Restore or delete the files and reload to undo a change.',
119
141
  parameters: Type.Object({}, { additionalProperties: false }),
@@ -147,10 +169,8 @@ export function createSelfEvolution(options) {
147
169
  else if (read.error.code !== 'not_found')
148
170
  errors.push({ path: INSTRUCTIONS, message: read.error.message });
149
171
  const skills = [];
150
- const skillFiles = await filesIn(env, SKILLS, '.md', context);
151
- if (typeof skillFiles === 'object' && !Array.isArray(skillFiles))
152
- errors.push({ path: SKILLS, message: skillFiles.error });
153
- for (const path of Array.isArray(skillFiles) ? skillFiles : []) {
172
+ const skillFiles = await skillFilesIn(env, context, errors);
173
+ for (const path of skillFiles) {
154
174
  const text = await env.readTextFile(path, context);
155
175
  if (!text.ok) {
156
176
  errors.push({ path, message: text.error.message });
@@ -168,7 +188,7 @@ export function createSelfEvolution(options) {
168
188
  errors.push({ path, message: `a skill named ${skill.name} already exists` });
169
189
  continue;
170
190
  }
171
- skills.push(skill);
191
+ skills.push(skillInFolder(skill, path.slice(0, path.lastIndexOf('/'))));
172
192
  }
173
193
  const available = typeof env.exec === 'function';
174
194
  const tools = [];
package/dist/skills.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface Skill {
7
7
  readonly description: string;
8
8
  readonly body: string;
9
9
  }
10
+ export declare function skillInFolder(skill: Skill, folder: string): Skill;
10
11
  /** Parse a `SKILL.md`-style document: `---` front matter with `name` and `description`, then the body. */
11
12
  export declare function parseSkill(markdown: string): Skill;
12
13
  export declare function validSkill(skill: Skill): Skill;
package/dist/skills.js CHANGED
@@ -2,6 +2,11 @@ import { defineExtension, defineTool } from '@earendil-works/pi-durable';
2
2
  import { Type } from '@earendil-works/pi-ai';
3
3
  /** Names of skills and agents: a lowercase letter or digit, then lowercase letters, digits, `.`, `_` or `-` (at most 64). */
4
4
  export const NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
5
+ const skillFolders = new WeakMap();
6
+ export function skillInFolder(skill, folder) {
7
+ skillFolders.set(skill, folder);
8
+ return skill;
9
+ }
5
10
  /** Parse a `SKILL.md`-style document: `---` front matter with `name` and `description`, then the body. */
6
11
  export function parseSkill(markdown) {
7
12
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(markdown);
@@ -23,7 +28,9 @@ export function validSkill(skill) {
23
28
  throw new TypeError(`Invalid skill name: ${skill.name}`);
24
29
  if (!skill.description.trim() || !skill.body.trim())
25
30
  throw new TypeError(`Skill ${skill.name} needs a description and a body`);
26
- return Object.freeze({ name: skill.name, description: skill.description.trim(), body: skill.body });
31
+ const valid = Object.freeze({ name: skill.name, description: skill.description.trim(), body: skill.body });
32
+ const folder = skillFolders.get(skill);
33
+ return folder === undefined ? valid : skillInFolder(valid, folder);
27
34
  }
28
35
  /** A prompt section listing the skills and a `load_skill` tool returning one body. Usable without `defineAgent`. */
29
36
  export function createSkillsExtension(name, skills) {
@@ -44,7 +51,9 @@ export function createSkillsExtension(name, skills) {
44
51
  replay: 'safe',
45
52
  execute: async (args) => {
46
53
  const skill = known.get(args.name);
47
- return { output: [{ type: 'text', text: skill ? skill.body : `Unknown skill "${args.name}". Available: ${[...known.keys()].join(', ')}` }] };
54
+ const folder = skill && skillFolders.get(skill);
55
+ const location = folder === undefined ? '' : `\n\nThis skill's folder is \`${folder}\`; paths in it are relative to that folder.`;
56
+ return { output: [{ type: 'text', text: skill ? skill.body + location : `Unknown skill "${args.name}". Available: ${[...known.keys()].join(', ')}` }] };
48
57
  },
49
58
  })],
50
59
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-agent",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "type": "module",
@@ -154,8 +154,8 @@
154
154
  "@earendil-works/pi-durable": "~1.2.0",
155
155
  "@earendil-works/pi-ai": "~1.2.0",
156
156
  "@earendil-works/chord": "~1.2.0",
157
- "@hachej/boring-files": "^0.4.4",
158
- "@hachej/boring-ui-kit": "^0.4.4"
157
+ "@hachej/boring-files": "^0.4.5",
158
+ "@hachej/boring-ui-kit": "^0.4.5"
159
159
  },
160
160
  "peerDependenciesMeta": {
161
161
  "@hachej/boring-files": {