@fastagent-sh/fastagent 0.24.1 → 0.24.3
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/dist/agent.d.ts +6 -0
- package/dist/agent.js +6 -0
- package/dist/cli/commands/add.js +1 -1
- package/dist/cli/commands/chat.js +1 -1
- package/dist/cli/commands/deploy.js +2 -2
- package/dist/cli/commands/dev.js +2 -2
- package/dist/cli/commands/info.js +12 -5
- package/dist/cli/commands/invoke.js +2 -3
- package/dist/cli/commands/routine-run.js +4 -5
- package/dist/cli/commands/start.js +2 -2
- package/dist/cli/kernel.js +1 -1
- package/dist/cli/program.js +4 -3
- package/dist/cli/shared.d.ts +28 -2
- package/dist/cli/shared.js +36 -13
- package/dist/core.d.ts +1 -1
- package/dist/core.js +5 -4
- package/dist/deploy/build-context.js +1 -1
- package/dist/deploy/preflight.js +9 -1
- package/dist/dev-supervisor.js +1 -1
- package/dist/engines/pi/agent-session-factory.d.ts +21 -5
- package/dist/engines/pi/agent-session-factory.js +134 -17
- package/dist/engines/pi/config.d.ts +6 -0
- package/dist/engines/pi/config.js +8 -0
- package/dist/engines/pi/create.d.ts +25 -30
- package/dist/engines/pi/create.js +64 -66
- package/dist/engines/pi/definition.d.ts +44 -28
- package/dist/engines/pi/definition.js +123 -30
- package/dist/engines/pi/invoke-session.d.ts +4 -1
- package/dist/engines/pi/invoke-session.js +12 -6
- package/dist/engines/pi/machine.d.ts +1 -2
- package/dist/engines/pi/machine.js +10 -1
- package/dist/engines/pi/open.d.ts +11 -7
- package/dist/engines/pi/open.js +13 -13
- package/dist/engines/pi/report.d.ts +7 -8
- package/dist/engines/pi/report.js +23 -5
- package/dist/engines/pi/session-builder.js +6 -2
- package/dist/engines/pi/session-control.d.ts +3 -2
- package/dist/engines/pi/session-control.js +23 -9
- package/dist/engines/pi/session-markers.d.ts +1 -1
- package/dist/engines/pi/session-markers.js +1 -1
- package/dist/engines/pi/session-settings.d.ts +14 -3
- package/dist/engines/pi/session-settings.js +20 -1
- package/dist/engines/pi/turn-kit.d.ts +31 -1
- package/dist/engines/pi/turn-kit.js +54 -1
- package/dist/paths.d.ts +1 -1
- package/dist/paths.js +12 -1
- package/dist/pi.d.ts +1 -1
- package/dist/scaffold/init.js +2 -2
- package/dist/scaffold/templates/APPEND_SYSTEM.md +12 -0
- package/dist/scaffold/templates/fastagent.config.ts +1 -1
- package/dist/session.d.ts +10 -0
- package/package.json +1 -1
- package/dist/scaffold/templates/persona.md +0 -12
|
@@ -3,19 +3,55 @@
|
|
|
3
3
|
* durable record, per invoke.
|
|
4
4
|
*/
|
|
5
5
|
import { dirname } from "node:path";
|
|
6
|
-
import { BUILTIN_EXTENSIONS, DISCOVERY, readMachine, withMachine } from "./machine.js";
|
|
7
|
-
import { ExtensionRunner, ModelRegistry, SessionManager, createAgentSessionServices, createCodemodeExtension, createToolSearchExtension, createAgentSessionFromServices, getAgentDir, initTheme, } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { BUILTIN_EXTENSIONS, DISCOVERY, readMachine, withMachine, } from "./machine.js";
|
|
7
|
+
import { ExtensionRunner, ModelRegistry, SessionManager, createAgentSessionServices, createCodemodeExtension, createToolSearchExtension, createAgentSessionFromServices, createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, getAgentDir, initTheme, } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { log } from "../../log.js";
|
|
9
9
|
import { getCurrentSystemMessage } from "@earendil-works/pi-ai";
|
|
10
10
|
import { resolveModel } from "./config.js";
|
|
11
|
-
import { activePath, resolveSessionSettings } from "./session-settings.js";
|
|
11
|
+
import { MissingModel, activePath, resolveSessionSettings } from "./session-settings.js";
|
|
12
12
|
import { DEFAULT_THINKING_LEVEL, withModelRegistration } from "./models.js";
|
|
13
13
|
import { registerAccountModels } from "./openai-account-models.js";
|
|
14
|
+
import { asksToEndRun, markEndsRun } from "./turn-kit.js";
|
|
14
15
|
import { agentSessionManager, sessionToolActivation, turnContext } from "./tool-context.js";
|
|
16
|
+
/**
|
|
17
|
+
* What pi's own coding tools say about themselves in its default prompt. The coding tools fastagent mounts are pi's
|
|
18
|
+
* `AgentTool`s, which carry neither field, and they replace pi's definitions of the same name, so without this pi's
|
|
19
|
+
* tool list would lose them and its rules would lose their guidelines.
|
|
20
|
+
*/
|
|
21
|
+
const PI_TOOL_PROMPTS = new Map([
|
|
22
|
+
createReadToolDefinition,
|
|
23
|
+
createBashToolDefinition,
|
|
24
|
+
createEditToolDefinition,
|
|
25
|
+
createWriteToolDefinition,
|
|
26
|
+
createGrepToolDefinition,
|
|
27
|
+
createFindToolDefinition,
|
|
28
|
+
createLsToolDefinition,
|
|
29
|
+
].map((create) => {
|
|
30
|
+
const definition = create(".");
|
|
31
|
+
return [
|
|
32
|
+
definition.name,
|
|
33
|
+
{ promptSnippet: definition.promptSnippet, promptGuidelines: definition.promptGuidelines },
|
|
34
|
+
];
|
|
35
|
+
}));
|
|
36
|
+
/**
|
|
37
|
+
* The line pi's default prompt lists a tool by. pi lists only tools that have one; an authored tool's is the first
|
|
38
|
+
* line of its description, so `defineTool` needs no field of its own.
|
|
39
|
+
*/
|
|
40
|
+
function promptFields(tool) {
|
|
41
|
+
const own = tool;
|
|
42
|
+
const pi = PI_TOOL_PROMPTS.get(tool.name);
|
|
43
|
+
const promptSnippet = own.promptSnippet ?? pi?.promptSnippet ?? tool.description?.split("\n")[0]?.trim();
|
|
44
|
+
const promptGuidelines = own.promptGuidelines ?? pi?.promptGuidelines;
|
|
45
|
+
return {
|
|
46
|
+
...(promptSnippet ? { promptSnippet } : {}),
|
|
47
|
+
...(promptGuidelines ? { promptGuidelines } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
15
50
|
/** fastagent's tools as pi tool definitions, bound to ONE session. */
|
|
16
51
|
function toolDefinitions(tools, bound, sessionId) {
|
|
17
52
|
return tools.map((tool) => ({
|
|
18
53
|
...tool,
|
|
54
|
+
...promptFields(tool),
|
|
19
55
|
label: tool.label || tool.name,
|
|
20
56
|
execute: (id, params, signal, onUpdate, ctx) => {
|
|
21
57
|
const binding = bound.current;
|
|
@@ -158,10 +194,33 @@ const nativeExtensions = BUILTIN_EXTENSIONS.map((name) => ({
|
|
|
158
194
|
replaceable: true,
|
|
159
195
|
builtin: true,
|
|
160
196
|
}));
|
|
197
|
+
/**
|
|
198
|
+
* FastAgent's own prompt sections, added on `before_agent_start`: pi renders custom sections after its own, so they
|
|
199
|
+
* survive a `SYSTEM.md` that replaced pi's default, and they stay out of the slot `APPEND_SYSTEM.md` fills.
|
|
200
|
+
*/
|
|
201
|
+
function fastagentSections(sections) {
|
|
202
|
+
return {
|
|
203
|
+
name: "fastagent-sections",
|
|
204
|
+
hidden: true,
|
|
205
|
+
factory: (pi) => {
|
|
206
|
+
pi.on("before_agent_start", (event) => {
|
|
207
|
+
Object.assign(event.systemPromptOptions.sections, sections);
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
161
212
|
/** The resource posture a fastagent definition asks pi for — ONE definition of it, for both assemblies. */
|
|
162
213
|
export function definitionResourceLoaderOptions(source) {
|
|
214
|
+
const { definition } = source;
|
|
163
215
|
return {
|
|
164
|
-
extensionFactories: [
|
|
216
|
+
extensionFactories: [
|
|
217
|
+
...nativeExtensions,
|
|
218
|
+
compactAdmission,
|
|
219
|
+
recordEndsRun,
|
|
220
|
+
...(definition.sections && Object.keys(definition.sections).length > 0
|
|
221
|
+
? [fastagentSections(definition.sections)]
|
|
222
|
+
: []),
|
|
223
|
+
],
|
|
165
224
|
// The machine's extensions are its owner's setup, not this agent's.
|
|
166
225
|
noExtensions: true,
|
|
167
226
|
// Explicit paths survive noExtensions, including Pi's built-ins; so they would also survive the machine's own
|
|
@@ -170,13 +229,14 @@ export function definitionResourceLoaderOptions(source) {
|
|
|
170
229
|
...source.machine.builtinExtensions.map((name) => `builtin:${name}`),
|
|
171
230
|
...(source.extensionPaths ?? []),
|
|
172
231
|
],
|
|
173
|
-
// Not pi's:
|
|
174
|
-
// definition.ts). Leaving both on would put every AGENTS.md in the prompt twice.
|
|
232
|
+
// Not pi's discovery: the definition read them (definition.ts), and pi renders what it is handed.
|
|
175
233
|
noContextFiles: true,
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
234
|
+
agentsFilesOverride: () => ({ agentsFiles: definition.contextFiles ?? [] }),
|
|
235
|
+
// The IDENTITY is the definition's, whatever the machine thinks: pi's `base` here is the machine's SYSTEM.md, and
|
|
236
|
+
// it is never used. Inheriting skills is inheriting capability; inheriting a system prompt would be the agent
|
|
237
|
+
// becoming someone else's agent. Undefined leaves pi to build its default.
|
|
238
|
+
systemPromptOverride: () => definition.systemPrompt,
|
|
239
|
+
appendSystemPromptOverride: () => (definition.appendSystemPrompt ? [definition.appendSystemPrompt] : []),
|
|
180
240
|
/**
|
|
181
241
|
* THE DEFINITION'S SKILLS, PLUS THE MACHINE'S — an agent inherits the box it runs on (machine.ts), and the
|
|
182
242
|
* definition wins a name collision.
|
|
@@ -186,13 +246,39 @@ export function definitionResourceLoaderOptions(source) {
|
|
|
186
246
|
*/
|
|
187
247
|
noSkills: true,
|
|
188
248
|
skillsOverride: () => ({
|
|
189
|
-
skills: withMachine(toPiSkills(
|
|
249
|
+
skills: withMachine(toPiSkills(definition.skills), source.machine.skills),
|
|
190
250
|
diagnostics: [],
|
|
191
251
|
}),
|
|
192
252
|
noPromptTemplates: true,
|
|
193
|
-
promptsOverride: () => ({
|
|
253
|
+
promptsOverride: () => ({
|
|
254
|
+
prompts: withMachine(toPiPrompts(definition.prompts ?? []), source.machine.prompts),
|
|
255
|
+
diagnostics: [],
|
|
256
|
+
}),
|
|
194
257
|
};
|
|
195
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Records on a tool result that it asked to end its run ({@link asksToEndRun}), so `entries()` can tell a run that
|
|
261
|
+
* ended on its tool batch from one cut after its tools. Through pi's `message_end` replacement, which runs before pi
|
|
262
|
+
* appends the message to the record. LAST among the extensions (pi appends inline factories after path ones), so a
|
|
263
|
+
* definition's own `message_end` replacement cannot drop the flag. One instance per loader, so per session.
|
|
264
|
+
*/
|
|
265
|
+
const recordEndsRun = {
|
|
266
|
+
name: "fastagent-record-ends-run",
|
|
267
|
+
hidden: true,
|
|
268
|
+
factory: (pi) => {
|
|
269
|
+
const asked = new Set();
|
|
270
|
+
pi.on("tool_execution_end", (event) => {
|
|
271
|
+
if (asksToEndRun(event))
|
|
272
|
+
asked.add(event.toolCallId);
|
|
273
|
+
});
|
|
274
|
+
pi.on("message_end", (event) => {
|
|
275
|
+
const message = event.message;
|
|
276
|
+
if (message.role !== "toolResult" || !asked.delete(message.toolCallId))
|
|
277
|
+
return;
|
|
278
|
+
return { message: markEndsRun(message) };
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
};
|
|
196
282
|
/** Manual compactions waiting for pi's admission, keyed by the session's record (what an extension's `ctx` names). */
|
|
197
283
|
const compactAdmissions = new WeakMap();
|
|
198
284
|
const COMPACT_ADMISSION = "fastagent-compact-admission";
|
|
@@ -269,8 +355,7 @@ export async function definitionServices(options) {
|
|
|
269
355
|
settingsManager: machine.settingsManager(),
|
|
270
356
|
resourceLoaderOptions: {
|
|
271
357
|
...definitionResourceLoaderOptions({
|
|
272
|
-
|
|
273
|
-
skills: () => definition.skills,
|
|
358
|
+
definition,
|
|
274
359
|
machine,
|
|
275
360
|
extensionPaths,
|
|
276
361
|
}),
|
|
@@ -317,18 +402,37 @@ export function piAgentSessionFactory(options) {
|
|
|
317
402
|
initTheme();
|
|
318
403
|
themeReady = true;
|
|
319
404
|
}
|
|
405
|
+
// Without a default, a turn that cannot run must leave no conversation behind in the listing. A new record with
|
|
406
|
+
// nothing to inherit records no model, so it is refused before it is created. One that inherits may get a model
|
|
407
|
+
// from its parent, which only the copy decides (the store's cut), so it is created and, if still model-less,
|
|
408
|
+
// removed below. The caller holds this session's lease, so nothing else creates the record in between.
|
|
409
|
+
const existed = options.modelSpec ? true : (await sessions.openIfExists(sessionId)) !== undefined;
|
|
410
|
+
if (!existed && !inherit)
|
|
411
|
+
throw new MissingModel(sessionId, []);
|
|
320
412
|
// Publish the record before loading resources: boundary writes must find it while binding is in flight.
|
|
321
413
|
const sessionManager = await sessions.openOrCreate(sessionId, inherit);
|
|
322
414
|
const definition = await options.readDefinition();
|
|
323
415
|
const { modelRuntime } = await options.engine();
|
|
324
416
|
const services = await definitionServices({ cwd, modelRuntime, definition, extensionPaths });
|
|
325
|
-
const model = resolveModel(modelRuntime, options.modelSpec);
|
|
417
|
+
const model = options.modelSpec ? resolveModel(modelRuntime, options.modelSpec) : undefined;
|
|
326
418
|
// What the session RUNS on: the boundary plane records model/thinking overrides as entries, and pi does not read
|
|
327
419
|
// them back.
|
|
328
|
-
const
|
|
329
|
-
|
|
420
|
+
const path = activePath(sessionManager);
|
|
421
|
+
const settings = resolveSessionSettings(path, modelRuntime, {
|
|
422
|
+
...(model ? { model } : {}),
|
|
330
423
|
thinkingLevel: thinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
331
424
|
});
|
|
425
|
+
if (!settings) {
|
|
426
|
+
const missing = new MissingModel(sessionId, path);
|
|
427
|
+
if (!existed) {
|
|
428
|
+
// The record this bind created for a thread whose parent supplied no model. A failed removal is reported on
|
|
429
|
+
// its own and does not replace `missing`: the caller still needs the missing_model answer and its fix.
|
|
430
|
+
await sessions.delete(sessionId).catch((error) => {
|
|
431
|
+
log.warn(`[fastagent] session ${sessionId}: could not remove the record of a refused turn: ${String(error)}`);
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
throw missing;
|
|
435
|
+
}
|
|
332
436
|
const { session } = await bindPiSession({
|
|
333
437
|
services,
|
|
334
438
|
sessionManager,
|
|
@@ -378,6 +482,19 @@ export function piAgentSessionFactory(options) {
|
|
|
378
482
|
return session;
|
|
379
483
|
};
|
|
380
484
|
}
|
|
485
|
+
/** The definition's prompt templates as pi's. */
|
|
486
|
+
function toPiPrompts(prompts) {
|
|
487
|
+
return prompts.map((prompt) => {
|
|
488
|
+
const baseDir = dirname(prompt.filePath);
|
|
489
|
+
return {
|
|
490
|
+
name: prompt.name,
|
|
491
|
+
description: prompt.description ?? "",
|
|
492
|
+
content: prompt.content,
|
|
493
|
+
filePath: prompt.filePath,
|
|
494
|
+
sourceInfo: { path: prompt.filePath, source: "fastagent", scope: "project", origin: "top-level", baseDir },
|
|
495
|
+
};
|
|
496
|
+
});
|
|
497
|
+
}
|
|
381
498
|
/** fastagent's Skill (content inline) as pi's (read from filePath at invocation time). */
|
|
382
499
|
function toPiSkills(skills) {
|
|
383
500
|
return skills.map((skill) => {
|
|
@@ -63,3 +63,9 @@ export declare function rewriteConfigModel(src: string, spec: string): string |
|
|
|
63
63
|
* would refuse to start an agent whose config names a model.
|
|
64
64
|
*/
|
|
65
65
|
export declare function resolveModelSpec(flag: string | undefined, config: FastagentConfig, env?: NodeJS.ProcessEnv): string | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* The refusal of a process that runs the agent here (the CLI's dev/start/invoke/routine run, and chat) when no
|
|
68
|
+
* default model resolves. The agent itself opens without one; such a process would fail every new conversation, so it
|
|
69
|
+
* stops at startup instead.
|
|
70
|
+
*/
|
|
71
|
+
export declare function missingDefaultModel(): Error;
|
|
@@ -189,3 +189,11 @@ export function rewriteConfigModel(src, spec) {
|
|
|
189
189
|
export function resolveModelSpec(flag, config, env = process.env) {
|
|
190
190
|
return flag ?? (env.FASTAGENT_MODEL || config.model);
|
|
191
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* The refusal of a process that runs the agent here (the CLI's dev/start/invoke/routine run, and chat) when no
|
|
194
|
+
* default model resolves. The agent itself opens without one; such a process would fail every new conversation, so it
|
|
195
|
+
* stops at startup instead.
|
|
196
|
+
*/
|
|
197
|
+
export function missingDefaultModel() {
|
|
198
|
+
return new Error(`missing model: set --model, "model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai-codex/gpt-5.5")`);
|
|
199
|
+
}
|
|
@@ -45,29 +45,15 @@ export declare function resolveAgentTools(config: FastagentConfig, agentDir: str
|
|
|
45
45
|
* asserts it (open.ts) — this function must stay reportable. */
|
|
46
46
|
toolSecrets: Map<string, DeclaredSecret[]>;
|
|
47
47
|
}>;
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* Base prompt (①), REQUIRED — no default: a defaulted piBasePrompt() would render "Available tools: (none)" even
|
|
58
|
-
* when tools are mounted.
|
|
59
|
-
*/
|
|
60
|
-
base: string;
|
|
61
|
-
/**
|
|
62
|
-
* ② project-context files (AGENTS.md et al. from loadProjectContextFiles); each wrapped `<project_instructions
|
|
63
|
-
* path=…>`.
|
|
64
|
-
*/
|
|
65
|
-
contextFiles?: Array<{
|
|
66
|
-
path: string;
|
|
67
|
-
content: string;
|
|
68
|
-
}>;
|
|
69
|
-
}
|
|
70
|
-
export declare function assembleSystemPrompt(options: AssembleSystemPromptOptions): string;
|
|
48
|
+
/**
|
|
49
|
+
* FastAgent's own prompt sections (docs/design/agent-model.md §2): what pi's prompt cannot know. Each is a named
|
|
50
|
+
* section pi renders after its own, so it holds whoever wrote the rest of the prompt.
|
|
51
|
+
*/
|
|
52
|
+
export declare function fastagentPromptSections(options: {
|
|
53
|
+
tools: readonly MountedTool[];
|
|
54
|
+
/** The {@link BUILTIN_EXTENSIONS} this machine leaves enabled (`Machine.builtinExtensions`). */
|
|
55
|
+
builtinExtensions: readonly string[];
|
|
56
|
+
}): Record<string, string>;
|
|
71
57
|
/** The assembly, as a value: what every rung builds and what the agent runs on. */
|
|
72
58
|
export interface PiAssembly {
|
|
73
59
|
lease: Lease;
|
|
@@ -76,10 +62,13 @@ export interface PiAssembly {
|
|
|
76
62
|
modelRuntime: () => Promise<ModelRuntime>;
|
|
77
63
|
/** A fresh runtime for every session, before loading its extensions. */
|
|
78
64
|
createModelRuntime: () => Promise<ModelRuntime>;
|
|
79
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* The registry and the default model, resolved on first use (a credential read is async). No model when the
|
|
67
|
+
* assembly has no default: each session then runs on the model it records (agent-session-factory.ts).
|
|
68
|
+
*/
|
|
80
69
|
engine: () => Promise<{
|
|
81
70
|
modelRuntime: ModelRuntime;
|
|
82
|
-
model
|
|
71
|
+
model?: AnyModel;
|
|
83
72
|
}>;
|
|
84
73
|
/** The configured reasoning effort — the other half of the pair a session without overrides runs on. */
|
|
85
74
|
thinkingLevel: ThinkingLevel;
|
|
@@ -101,8 +90,8 @@ export interface CreatePiAgentOptions {
|
|
|
101
90
|
/** Reasoning effort (pi's scale). */
|
|
102
91
|
thinkingLevel?: ThinkingLevel;
|
|
103
92
|
/**
|
|
104
|
-
* The system prompt itself — no engine base and no wrapping (unlike the directory path,
|
|
105
|
-
*
|
|
93
|
+
* The system prompt itself — no engine base and no wrapping (unlike the directory path, where pi builds its default
|
|
94
|
+
* prompt, or SYSTEM.md replaces it, and AGENTS.md, APPEND_SYSTEM.md and FastAgent's sections are added).
|
|
106
95
|
*/
|
|
107
96
|
instructions?: string | (() => string);
|
|
108
97
|
/** The tool set to mount: authored tools or pi's cwd-bound coding tools, both AgentTool. */
|
|
@@ -129,11 +118,17 @@ export interface CreatePiAgentOptions {
|
|
|
129
118
|
export declare function createPiAgent(options: CreatePiAgentOptions): Agent;
|
|
130
119
|
/** L2 options. */
|
|
131
120
|
export interface CreatePiAgentFromDefinitionOptions {
|
|
132
|
-
/**
|
|
133
|
-
|
|
121
|
+
/**
|
|
122
|
+
* The default model spec "provider/modelId", resolved against {@link models}. Optional: without it, a session runs
|
|
123
|
+
* on the model it records, and one that records none fails its invoke with `missing_model`.
|
|
124
|
+
*/
|
|
125
|
+
model?: string;
|
|
134
126
|
/** Reasoning effort (pi's scale). */
|
|
135
127
|
thinkingLevel?: ThinkingLevel;
|
|
136
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Replaces pi's default prompt, as `SYSTEM.md` does, and outranks it. Required when `tools` replaces the coding
|
|
130
|
+
* tools and the directory has no `SYSTEM.md`: pi's default would claim tools the agent does not have.
|
|
131
|
+
*/
|
|
137
132
|
base?: string;
|
|
138
133
|
tools?: MountedTool[];
|
|
139
134
|
/**
|
|
@@ -145,70 +145,57 @@ export async function resolveAgentTools(config, agentDir, cwd) {
|
|
|
145
145
|
]),
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
|
-
//
|
|
149
|
-
/** The pi
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
// pi builds the prompt (its default, or SYSTEM.md in its place); FastAgent adds its own named sections after it.
|
|
149
|
+
/** The tools pi's default prompt says the agent has ("reading files, executing commands, editing code"). */
|
|
150
|
+
const CODING_IDENTITY_TOOLS = ["read", "bash", "edit", "write"];
|
|
151
|
+
/**
|
|
152
|
+
* Refuse a prompt that would claim tools the agent does not have: pi's default says the agent reads files, runs
|
|
153
|
+
* commands and edits code, which an embedder that replaced the coding tools made false. A prompt of the agent's own
|
|
154
|
+
* (`base`, or `SYSTEM.md`) describes it instead. Checked at assembly and on every turn, since `SYSTEM.md` is re-read
|
|
155
|
+
* per turn and an agent may delete it.
|
|
156
|
+
*/
|
|
157
|
+
function refuseDefaultPromptOverReplacedTools(tools, hasOwnPrompt) {
|
|
158
|
+
if (hasOwnPrompt)
|
|
159
|
+
return;
|
|
160
|
+
const mounted = new Set(tools.map((tool) => tool.name));
|
|
161
|
+
const missing = CODING_IDENTITY_TOOLS.filter((name) => !mounted.has(name));
|
|
162
|
+
if (missing.length === 0)
|
|
163
|
+
return;
|
|
164
|
+
throw new Error(`the coding tools were replaced (${missing.join(", ")} not mounted), so pi's default prompt, which says the agent ` +
|
|
165
|
+
`reads files, runs commands and edits code, does not describe this agent: pass \`base\` or write SYSTEM.md`);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* FastAgent's own prompt sections (docs/design/agent-model.md §2): what pi's prompt cannot know. Each is a named
|
|
169
|
+
* section pi renders after its own, so it holds whoever wrote the rest of the prompt.
|
|
170
|
+
*/
|
|
171
|
+
export function fastagentPromptSections(options) {
|
|
172
|
+
const sections = {};
|
|
173
|
+
const mountedNames = new Set(options.tools.map((tool) => tool.name));
|
|
156
174
|
// Only `deferred` tools are tool_search's to load (the session activates it for them); `codemode` ones are listed
|
|
157
175
|
// in codemode's own description, and an inactive direct tool is reached only through an authored activation.
|
|
158
176
|
// A disabled tool-search leaves them no way in, and naming tool_search would invite calls to a tool that is not there.
|
|
159
|
-
const deferredCount =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const mountedNames = new Set(mounted.map((tool) => tool.name));
|
|
164
|
-
const fullCodingSurface = ["read", "bash", "edit", "write"].every((name) => mountedNames.has(name));
|
|
165
|
-
const identity = options.persona?.trim() ||
|
|
166
|
-
(fullCodingSurface
|
|
167
|
-
? "You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files."
|
|
168
|
-
: "You are an AI assistant operating inside pi, an agent harness. Help users using only the tools and context available to you.");
|
|
169
|
-
const deferredNote = deferredCount > 0
|
|
170
|
-
? `\n\n${deferredCount} additional tool(s) are registered but not loaded — use tool_search to find and load them before concluding a capability is missing.`
|
|
171
|
-
: "";
|
|
177
|
+
const deferredCount = options.tools.filter((tool) => tool.exposure === "deferred" && hasWayIn(tool, options.builtinExtensions)).length;
|
|
178
|
+
if (deferredCount > 0) {
|
|
179
|
+
sections.deferred_tools = `${deferredCount} additional tool(s) are registered but not loaded — use tool_search to find and load them before concluding a capability is missing.`;
|
|
180
|
+
}
|
|
172
181
|
// What takes effect when, and the agent's own path to a new capability, are the same on every host; only how long
|
|
173
182
|
// the storage lives differs, below. The skill it writes lives in the definition directory, which the next
|
|
174
183
|
// deployment replaces (or, on AgentCore, erases with everything else) — so the sentence says so, and where a skill
|
|
175
184
|
// that should outlast it has to go. A skill outside the definition would survive, but it is machine state, read
|
|
176
185
|
// once per process (machine.ts), so it would not be live.
|
|
177
|
-
const
|
|
178
|
-
// Named only when mounted (a serve; not a one-shot invoke), like the deferred tools above: naming a tool the model
|
|
179
|
-
// have invites calls to it.
|
|
186
|
+
const runtimeChanges = ` Markdown definition files are read each turn; changes to tools, channels or configuration take effect when the service restarts. To give yourself a new capability now, write a skill — a SKILL.md in your definition's skills/ directory, with any script it needs run through bash. It lasts until the next deployment replaces that directory, so a capability that should outlast it belongs in the author's release: propose it to them.${
|
|
187
|
+
// Named only when mounted (a serve; not a one-shot invoke), like the deferred tools above: naming a tool the model
|
|
188
|
+
// does not have invites calls to it.
|
|
180
189
|
mountedNames.has("wake") ? " To schedule your own follow-up work, use the wake tool." : ""}`;
|
|
181
|
-
// How long the storage lives is the HOST's answer, not a deployment-wide one: AgentCore's managed
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
: `\n\nYour workspace survives restarts and deployments, including uncommitted work; /tmp does not. A new deployment replaces your definition directory with the author's release, so keep ongoing project work outside it.${RUNTIME_CHANGES}`;
|
|
189
|
-
return `${identity}
|
|
190
|
-
|
|
191
|
-
Available tools:
|
|
192
|
-
${toolsList}${deferredNote}
|
|
193
|
-
|
|
194
|
-
In addition to the tools above, you may have access to other custom tools depending on the project.
|
|
195
|
-
|
|
196
|
-
Guidelines:
|
|
197
|
-
- Be concise in your responses
|
|
198
|
-
- Show file paths clearly when working with files${deploymentNote}`;
|
|
199
|
-
}
|
|
200
|
-
export function assembleSystemPrompt(options) {
|
|
201
|
-
let prompt = options.base;
|
|
202
|
-
const contextFiles = options.contextFiles ?? [];
|
|
203
|
-
if (contextFiles.length > 0) {
|
|
204
|
-
// Mirrors pi's system-prompt.js: one <project_context> block, one <project_instructions path=…> per file.
|
|
205
|
-
prompt += `\n\n<project_context>\n\nProject-specific instructions and guidelines:\n\n`;
|
|
206
|
-
for (const { path, content } of contextFiles) {
|
|
207
|
-
prompt += `<project_instructions path="${path}">\n${content}\n</project_instructions>\n\n`;
|
|
208
|
-
}
|
|
209
|
-
prompt += `</project_context>\n`;
|
|
190
|
+
// How long the storage lives is the HOST's answer, not a deployment-wide one: AgentCore's managed mount is reset by
|
|
191
|
+
// every deploy, so telling that agent to keep work "outside the definition" would name a location its next deploy
|
|
192
|
+
// erases.
|
|
193
|
+
if (isDeployedWorkspace()) {
|
|
194
|
+
sections.self_change = isAgentcoreRuntime()
|
|
195
|
+
? `Your workspace survives restarts, including uncommitted work; /tmp does not. Every deployment of a new version resets this host's storage entirely, so anything that must outlive a deployment belongs in an external system (a git remote, an issue tracker, a database).${runtimeChanges}`
|
|
196
|
+
: `Your workspace survives restarts and deployments, including uncommitted work; /tmp does not. A new deployment replaces your definition directory with the author's release, so keep ongoing project work outside it.${runtimeChanges}`;
|
|
210
197
|
}
|
|
211
|
-
return
|
|
198
|
+
return sections;
|
|
212
199
|
}
|
|
213
200
|
/** Shared low-level wiring: resolve the model spec against the collection, default the K ports, build the parts. */
|
|
214
201
|
function assemblePi(opts) {
|
|
@@ -225,7 +212,11 @@ function assemblePi(opts) {
|
|
|
225
212
|
};
|
|
226
213
|
let engine;
|
|
227
214
|
const resolveEngine = () => {
|
|
228
|
-
|
|
215
|
+
const spec = opts.model;
|
|
216
|
+
engine ??= modelRuntime().then((runtime) => ({
|
|
217
|
+
modelRuntime: runtime,
|
|
218
|
+
...(spec ? { model: resolveModel(runtime, spec) } : {}),
|
|
219
|
+
}));
|
|
229
220
|
return engine;
|
|
230
221
|
};
|
|
231
222
|
// Deny omitted coding names so discovery cannot reintroduce tools a lower-level caller excluded.
|
|
@@ -233,7 +224,7 @@ function assemblePi(opts) {
|
|
|
233
224
|
const sessionFactory = piAgentSessionFactory({
|
|
234
225
|
sessions,
|
|
235
226
|
engine: async () => ({ modelRuntime: await createModelRuntime() }),
|
|
236
|
-
modelSpec: opts.model,
|
|
227
|
+
...(opts.model ? { modelSpec: opts.model } : {}),
|
|
237
228
|
thinkingLevel: opts.thinkingLevel,
|
|
238
229
|
tools: opts.tools,
|
|
239
230
|
readDefinition: opts.readDefinition,
|
|
@@ -267,8 +258,9 @@ export function createPiAgent(options) {
|
|
|
267
258
|
thinkingLevel: options.thinkingLevel,
|
|
268
259
|
models: models.createRuntime,
|
|
269
260
|
catalog: models.runtime,
|
|
261
|
+
// No instructions is an empty prompt, not pi's default: this path takes the prompt whole.
|
|
270
262
|
readDefinition: () => ({
|
|
271
|
-
systemPrompt: typeof instructions === "function" ? instructions() : instructions,
|
|
263
|
+
systemPrompt: (typeof instructions === "function" ? instructions() : instructions) || " ",
|
|
272
264
|
skills,
|
|
273
265
|
}),
|
|
274
266
|
tools: options.tools,
|
|
@@ -283,8 +275,8 @@ export function createPiAgent(options) {
|
|
|
283
275
|
* is built here from the credential options.
|
|
284
276
|
*/
|
|
285
277
|
export async function assemblePiFromDefinition(dir, options) {
|
|
286
|
-
// `dir` = the agent-definition dir
|
|
287
|
-
//
|
|
278
|
+
// `dir` = the agent-definition dir; `cwd` (default = dir) is the run root where tools operate and whose ancestors are
|
|
279
|
+
// walked for project context.
|
|
288
280
|
const cwd = options.cwd ?? dir;
|
|
289
281
|
const env = options.env ?? new NodeExecutionEnv({ cwd });
|
|
290
282
|
// Boot-time load: fail-visibly at startup on a broken directory, and give callers the snapshot to report
|
|
@@ -294,6 +286,12 @@ export async function assemblePiFromDefinition(dir, options) {
|
|
|
294
286
|
// Boot findings go through the SAME memoized reporter every later reader uses (report.ts, keyed by the resolved
|
|
295
287
|
// dir).
|
|
296
288
|
reportFindingsIfChanged(definition.dir, definition);
|
|
289
|
+
// pi treats an empty prompt as none and builds its default, so a blank `base` would look like a prompt of the
|
|
290
|
+
// caller's own while being nothing.
|
|
291
|
+
if (options.base !== undefined && options.base.trim() === "") {
|
|
292
|
+
throw new Error("`base` is empty: pass the prompt the agent should use, or leave `base` out for pi's default");
|
|
293
|
+
}
|
|
294
|
+
refuseDefaultPromptOverReplacedTools(tools, options.base !== undefined || definition.systemPrompt !== undefined);
|
|
297
295
|
const { providers } = options;
|
|
298
296
|
const models = options.models ?? agentModels(dir, options, { cwd, env, ...(providers ? { providers } : {}) });
|
|
299
297
|
// Built at boot, so a malformed models.json fails the assembly rather than its first turn. The directory's own
|
|
@@ -317,15 +315,15 @@ export async function assemblePiFromDefinition(dir, options) {
|
|
|
317
315
|
readDefinition: async () => {
|
|
318
316
|
const def = await loadAgentDefinition(dir, { cwd, env });
|
|
319
317
|
reportFindingsIfChanged(def.dir, def);
|
|
318
|
+
const systemPrompt = options.base ?? def.systemPrompt?.content;
|
|
319
|
+
refuseDefaultPromptOverReplacedTools(tools, systemPrompt !== undefined);
|
|
320
320
|
return {
|
|
321
|
-
systemPrompt:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// ② project context: AGENTS.md files (agentDir + cwd-ancestor walk) via loadProjectContextFiles.
|
|
326
|
-
contextFiles: def.contextFiles,
|
|
327
|
-
}),
|
|
321
|
+
...(systemPrompt !== undefined ? { systemPrompt } : {}),
|
|
322
|
+
...(def.appendSystemPrompt ? { appendSystemPrompt: def.appendSystemPrompt.content } : {}),
|
|
323
|
+
contextFiles: def.contextFiles,
|
|
324
|
+
sections: fastagentPromptSections({ tools, builtinExtensions }),
|
|
328
325
|
skills: def.skills,
|
|
326
|
+
prompts: def.prompts,
|
|
329
327
|
};
|
|
330
328
|
},
|
|
331
329
|
tools,
|
|
@@ -1,39 +1,68 @@
|
|
|
1
|
-
import { type ExecutionEnv, type Skill, type SkillDiagnostic } from "@earendil-works/pi-agent-core";
|
|
1
|
+
import { type ExecutionEnv, type PromptTemplateDiagnostic, type Skill, type SkillDiagnostic } from "@earendil-works/pi-agent-core";
|
|
2
2
|
/** A same-name skill collision (the discarded side). */
|
|
3
3
|
export interface SkillCollision {
|
|
4
4
|
name: string;
|
|
5
5
|
winnerPath: string;
|
|
6
6
|
loserPath: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Two places in the definition holding the same thing: `SYSTEM.md` and `.pi/SYSTEM.md`, or one prompt template in
|
|
10
|
+
* `prompts/` and `.pi/prompts/`. The first is used; the other is reported, because it is a file the author wrote that
|
|
11
|
+
* does nothing.
|
|
12
|
+
*/
|
|
13
|
+
export interface DefinitionShadow {
|
|
14
|
+
what: string;
|
|
15
|
+
winnerPath: string;
|
|
16
|
+
loserPath: string;
|
|
17
|
+
}
|
|
18
|
+
/** A file the definition holds verbatim, with where it came from. */
|
|
19
|
+
export interface DefinitionFile {
|
|
20
|
+
path: string;
|
|
21
|
+
content: string;
|
|
22
|
+
}
|
|
23
|
+
/** A prompt template of the definition's own (`prompts/`, `.pi/prompts/`). */
|
|
24
|
+
export interface DefinitionPrompt {
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
content: string;
|
|
28
|
+
filePath: string;
|
|
29
|
+
}
|
|
30
|
+
export type DefinitionDiagnostic = SkillDiagnostic | PromptTemplateDiagnostic;
|
|
8
31
|
/** Result of loading a definition directory. */
|
|
9
32
|
export interface LoadedDefinition {
|
|
10
|
-
/** Project-context files
|
|
33
|
+
/** Project-context files (AGENTS.md), sourced via pi's `loadProjectContextFiles`; pi renders them. */
|
|
11
34
|
contextFiles: Array<{
|
|
12
35
|
path: string;
|
|
13
36
|
content: string;
|
|
14
37
|
}>;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*/
|
|
20
|
-
persona?: string;
|
|
38
|
+
/** `SYSTEM.md`, else `.pi/SYSTEM.md`: replaces pi's default prompt. Absent → pi builds its default. */
|
|
39
|
+
systemPrompt?: DefinitionFile;
|
|
40
|
+
/** `APPEND_SYSTEM.md`, else `.pi/APPEND_SYSTEM.md`: added after the prompt, whichever it is. */
|
|
41
|
+
appendSystemPrompt?: DefinitionFile;
|
|
21
42
|
skills: Skill[];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
prompts: DefinitionPrompt[];
|
|
44
|
+
/** Non-fatal per-file problems reported by pi's skill and prompt-template loaders. */
|
|
45
|
+
diagnostics: DefinitionDiagnostic[];
|
|
46
|
+
/** Same-name skill conflicts across the definition's skill directories (first wins). */
|
|
25
47
|
collisions: SkillCollision[];
|
|
26
|
-
/**
|
|
48
|
+
/** Same-name files and prompt templates across the definition's locations (first wins). */
|
|
49
|
+
shadowed: DefinitionShadow[];
|
|
50
|
+
/** Paths that exist in the definition and are deliberately not loaded, each with why. */
|
|
51
|
+
ignored: Array<{
|
|
52
|
+
path: string;
|
|
53
|
+
reason: string;
|
|
54
|
+
}>;
|
|
55
|
+
/** Absolute agent-definition directory path. */
|
|
27
56
|
dir: string;
|
|
28
57
|
}
|
|
29
58
|
export interface LoadAgentDefinitionOptions {
|
|
30
|
-
/** Working directory whose ancestors are walked for context files
|
|
59
|
+
/** Working directory whose ancestors are walked for context files. */
|
|
31
60
|
cwd?: string;
|
|
32
61
|
env?: ExecutionEnv;
|
|
33
62
|
}
|
|
34
63
|
/**
|
|
35
|
-
* Read an agent definition
|
|
36
|
-
* agentDir }).
|
|
64
|
+
* Read an agent definition: its prompt files, skills and prompt templates from `agentDir`; the project context from
|
|
65
|
+
* pi's loadProjectContextFiles({ cwd, agentDir }).
|
|
37
66
|
*/
|
|
38
67
|
export declare function loadAgentDefinition(agentDir: string, options?: LoadAgentDefinitionOptions): Promise<LoadedDefinition>;
|
|
39
68
|
/** Extension entry-point FILES under `<agentDir>/extensions/`, empty when there are none. */
|
|
@@ -41,18 +70,5 @@ export declare function loadExtensionPaths(agentDir: string, options?: {
|
|
|
41
70
|
cwd?: string;
|
|
42
71
|
env?: ExecutionEnv;
|
|
43
72
|
}): Promise<string[]>;
|
|
44
|
-
/**
|
|
45
|
-
* The definition's skills ALONE, resolved the same way `loadAgentDefinition` resolves them (same loader, same
|
|
46
|
-
* containment guard, same first-wins collision rule).
|
|
47
|
-
*/
|
|
48
|
-
export declare function loadAgentSkills(agentDir: string, options?: {
|
|
49
|
-
cwd?: string;
|
|
50
|
-
env?: ExecutionEnv;
|
|
51
|
-
}): Promise<{
|
|
52
|
-
skills: Skill[];
|
|
53
|
-
diagnostics: SkillDiagnostic[];
|
|
54
|
-
collisions: SkillCollision[];
|
|
55
|
-
dir: string;
|
|
56
|
-
}>;
|
|
57
73
|
/** Resolve to a canonical (symlink-free) absolute path so comparisons match `process.cwd()`'s realpath. */
|
|
58
74
|
export declare function canonicalPath(p: string): string;
|