pi-open-agents 0.1.31 → 0.1.33
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 +7 -4
- package/package.json +1 -1
- package/src/discovery/loader.ts +119 -0
- package/src/subagent/prompt.ts +2 -2
- package/src/subagent/tool.ts +32 -2
package/README.md
CHANGED
|
@@ -334,9 +334,11 @@ Use `replace-all` for fully isolated agents that should not be influenced by
|
|
|
334
334
|
workspace context — e.g., a subagent that must run identically regardless of
|
|
335
335
|
the project it's invoked from.
|
|
336
336
|
|
|
337
|
-
For **subagents** (child process), the
|
|
338
|
-
|
|
339
|
-
the body.
|
|
337
|
+
For **subagents** (child process), the same modes apply: with the default
|
|
338
|
+
`append`, the agent body is appended to pi's default prompt (`--append-system-prompt`);
|
|
339
|
+
`replace`/`replace-all` pass the body as the whole system prompt (`--system-prompt`).
|
|
340
|
+
Skills are injected as an XML block after the body, followed by the result-closing
|
|
341
|
+
contract (`## Outcome` block, file + summary).
|
|
340
342
|
|
|
341
343
|
---
|
|
342
344
|
|
|
@@ -415,7 +417,8 @@ six sessions, zero chaos: that's the exact workstation setup documented in
|
|
|
415
417
|
- ~~**Per-call model override** in `subagent()`~~ — ✅ shipped in [0.1.19](https://github.com/andrea-tomassi/pi-open-agents/releases/tag/v0.1.19) (#10)
|
|
416
418
|
- ~~**Primary tool filters incl. extension tools**~~ — ✅ shipped in [0.1.30](https://github.com/andrea-tomassi/pi-open-agents/releases/tag/v0.1.30) (#18)
|
|
417
419
|
- ~~**Subagent output: file + summary**~~ — ✅ shipped in [0.1.31](https://github.com/andrea-tomassi/pi-open-agents/releases/tag/v0.1.31) (#32)
|
|
418
|
-
-
|
|
420
|
+
- ~~**Parallel subagent limit**~~ — ✅ closed, see #11 for the outcome
|
|
421
|
+
- **Zero open issues** — the tracker is empty; new work starts from new issues.
|
|
419
422
|
|
|
420
423
|
---
|
|
421
424
|
|
package/package.json
CHANGED
package/src/discovery/loader.ts
CHANGED
|
@@ -115,6 +115,12 @@ export async function loadAgents(options: LoadAgentsOptions = {}): Promise<Agent
|
|
|
115
115
|
// Skip disabled agents entirely
|
|
116
116
|
if (agent.disable) continue;
|
|
117
117
|
|
|
118
|
+
// Resolve body-level include directives (fail-soft, never throws)
|
|
119
|
+
agent.prompt = await resolveIncludes(agent.prompt, path.dirname(filePath), {
|
|
120
|
+
fs: fileSystem,
|
|
121
|
+
reportedMissing: new Set(),
|
|
122
|
+
}, new Set([path.resolve(filePath)]), 0);
|
|
123
|
+
|
|
118
124
|
// Add to the appropriate map
|
|
119
125
|
if (discPath.source === "project") {
|
|
120
126
|
projectAgents.set(agent.name, agent);
|
|
@@ -153,6 +159,119 @@ function defaultAgentDir(): string {
|
|
|
153
159
|
return path.join(process.env.HOME ?? "", ".pi", "agent");
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
// ─── Include Directive Resolution ───────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
/** Max recursion depth for include directives. */
|
|
165
|
+
const INCLUDE_MAX_DEPTH = 8;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A full-line include directive: `include: "path"`, `include: 'path'`,
|
|
169
|
+
* or `include: path`. Case-sensitive, must start at column 0 and occupy
|
|
170
|
+
* the whole line (trailing whitespace allowed). Occurrences inside
|
|
171
|
+
* sentences, indented lines, or fenced code blocks never match.
|
|
172
|
+
*/
|
|
173
|
+
const INCLUDE_LINE_RE = /^include:[ \t]+(?:"([^"]*)"|'([^']*)'|([^\s"']+))[ \t]*$/;
|
|
174
|
+
|
|
175
|
+
/** Opening/closing fenced code block markers (``` or ~~~, optionally indented). */
|
|
176
|
+
const FENCE_RE = /^[ \t]*(```|~~~)/;
|
|
177
|
+
|
|
178
|
+
/** Shared state for one loadAgents run. */
|
|
179
|
+
interface IncludeContext {
|
|
180
|
+
/** Filesystem abstraction (honors the custom fs used for tests). */
|
|
181
|
+
fs: AgentFs;
|
|
182
|
+
/** Resolved paths already warned about (one warning per missing path). */
|
|
183
|
+
reportedMissing: Set<string>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function warnInclude(message: string): void {
|
|
187
|
+
console.error(`[pi-open-agents] include: ${message}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolve body-level include directives in agent markdown.
|
|
192
|
+
*
|
|
193
|
+
* Each directive line is replaced by the content of the referenced file,
|
|
194
|
+
* resolved relative to the directory of the file containing the directive.
|
|
195
|
+
* Includes are recursive under the same rule. Cycles are detected via the
|
|
196
|
+
* set of absolute paths on the current expansion path, and nesting is
|
|
197
|
+
* capped at INCLUDE_MAX_DEPTH.
|
|
198
|
+
*
|
|
199
|
+
* Fail-soft: missing files, cycles, and over-deep nesting keep the directive
|
|
200
|
+
* line literal and warn on stderr, but never throw. Keeping the line (instead
|
|
201
|
+
* of dropping it) preserves visibility for debugging and for any downstream
|
|
202
|
+
* loader that might resolve it later.
|
|
203
|
+
*
|
|
204
|
+
* @param content Markdown body to process
|
|
205
|
+
* @param baseDir Directory of the file containing the directives
|
|
206
|
+
* @param ctx Shared per-load state
|
|
207
|
+
* @param visited Absolute paths on the current expansion path (cycle guard)
|
|
208
|
+
* @param depth Current inclusion depth (0 = the agent file itself)
|
|
209
|
+
*/
|
|
210
|
+
async function resolveIncludes(
|
|
211
|
+
content: string,
|
|
212
|
+
baseDir: string,
|
|
213
|
+
ctx: IncludeContext,
|
|
214
|
+
visited: Set<string>,
|
|
215
|
+
depth: number,
|
|
216
|
+
): Promise<string> {
|
|
217
|
+
const lines = content.split("\n");
|
|
218
|
+
const out: string[] = [];
|
|
219
|
+
let inFence = false;
|
|
220
|
+
|
|
221
|
+
for (const line of lines) {
|
|
222
|
+
if (FENCE_RE.test(line)) {
|
|
223
|
+
inFence = !inFence;
|
|
224
|
+
out.push(line);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (inFence) {
|
|
229
|
+
out.push(line);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const match = INCLUDE_LINE_RE.exec(line);
|
|
234
|
+
const includePath = match ? (match[1] ?? match[2] ?? match[3]) : undefined;
|
|
235
|
+
if (!includePath) {
|
|
236
|
+
out.push(line);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const resolved = path.resolve(baseDir, includePath);
|
|
241
|
+
|
|
242
|
+
if (visited.has(resolved)) {
|
|
243
|
+
warnInclude(`cycle detected for "${includePath}" (${resolved}); keeping directive literal`);
|
|
244
|
+
out.push(line);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (depth >= INCLUDE_MAX_DEPTH) {
|
|
249
|
+
warnInclude(`max depth ${INCLUDE_MAX_DEPTH} exceeded for "${includePath}"; keeping directive literal`);
|
|
250
|
+
out.push(line);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let raw: string;
|
|
255
|
+
try {
|
|
256
|
+
raw = await ctx.fs.readFile(resolved);
|
|
257
|
+
} catch {
|
|
258
|
+
if (!ctx.reportedMissing.has(resolved)) {
|
|
259
|
+
ctx.reportedMissing.add(resolved);
|
|
260
|
+
warnInclude(`file not found: "${includePath}" (${resolved}); keeping directive literal`);
|
|
261
|
+
}
|
|
262
|
+
out.push(line);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
visited.add(resolved);
|
|
267
|
+
const expanded = await resolveIncludes(raw, path.dirname(resolved), ctx, visited, depth + 1);
|
|
268
|
+
visited.delete(resolved);
|
|
269
|
+
out.push(expanded);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return out.join("\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
156
275
|
// ─── Filtering Utilities ────────────────────────────────────────────────────
|
|
157
276
|
|
|
158
277
|
/**
|
package/src/subagent/prompt.ts
CHANGED
|
@@ -78,8 +78,8 @@ export function buildClosingContract(outputPath?: string): string {
|
|
|
78
78
|
"",
|
|
79
79
|
"If you produced files the caller asked for, list each on its own line as",
|
|
80
80
|
"`<path>` — <one line describing the content>.",
|
|
81
|
-
"Keep the outcome block to 2-3 lines.
|
|
82
|
-
"
|
|
81
|
+
"Keep the outcome block to 2-3 lines. Keep the rest of your answer concise — the",
|
|
82
|
+
"outcome block is what the caller reads first; never hide an error behind brevity.",
|
|
83
83
|
];
|
|
84
84
|
if (outputPath) {
|
|
85
85
|
lines.push(
|
package/src/subagent/tool.ts
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
parentInheritedRules,
|
|
46
46
|
type RecursionEnv,
|
|
47
47
|
} from "./env.ts";
|
|
48
|
-
import { preview, shortenPath, stringArg } from "../utils/format.ts";
|
|
48
|
+
import { byteLength, preview, shortenPath, stringArg } from "../utils/format.ts";
|
|
49
49
|
|
|
50
50
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
51
51
|
|
|
@@ -161,11 +161,41 @@ function toToolResult(result: AgentResult) {
|
|
|
161
161
|
|
|
162
162
|
function toProgressResult(progress: AgentProgress) {
|
|
163
163
|
return {
|
|
164
|
-
content: [{ type: "text" as const, text: progress.output
|
|
164
|
+
content: [{ type: "text" as const, text: liveProgressText(progress.output) }],
|
|
165
165
|
details: progress,
|
|
166
166
|
};
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Cap for live progress text (issue #32 follow-up).
|
|
171
|
+
*
|
|
172
|
+
* Every progress emit used to re-send the child's ENTIRE accumulated output:
|
|
173
|
+
* over a long run the same bytes go out N times (once per event, growing).
|
|
174
|
+
* Partial updates are transient (never persisted to the transcript — verified
|
|
175
|
+
* against live session JSONL), so the cap only trims live display/RPC traffic:
|
|
176
|
+
* the tail stays (what the child is doing *now*), the head is already in the
|
|
177
|
+
* temp archive. Final results are untouched (see toToolResult).
|
|
178
|
+
*/
|
|
179
|
+
export const LIVE_PROGRESS_MAX_BYTES = 2048;
|
|
180
|
+
|
|
181
|
+
/** Tail a string to a byte budget (halving converge, multibyte-safe). */
|
|
182
|
+
export function tailTextByBytes(text: string, maxBytes: number): string {
|
|
183
|
+
if (byteLength(text) <= maxBytes) return text;
|
|
184
|
+
let take = Math.min(text.length, maxBytes);
|
|
185
|
+
let cut = text.slice(text.length - take);
|
|
186
|
+
while (byteLength(cut) > maxBytes && take > 0) {
|
|
187
|
+
take = Math.floor(take / 2);
|
|
188
|
+
cut = text.slice(text.length - take);
|
|
189
|
+
}
|
|
190
|
+
return cut;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function liveProgressText(output: string | undefined): string {
|
|
194
|
+
const text = output || "(running...)";
|
|
195
|
+
const tailed = tailTextByBytes(text, LIVE_PROGRESS_MAX_BYTES);
|
|
196
|
+
return tailed.length < text.length ? `[…earlier live output omitted…]\n${tailed}` : text;
|
|
197
|
+
}
|
|
198
|
+
|
|
169
199
|
// ─── Styled Rendering ────────────────────────────────────────────────────────
|
|
170
200
|
|
|
171
201
|
function styledPathArg(args: Record<string, unknown>, theme: ThemeLike, fallback?: string): string {
|