fix(coding-agent): chain system prompt in before_agent_start closes #3539
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `ctx.getSystemPrompt()` inside `before_agent_start` to reflect chained system-prompt changes made by earlier `before_agent_start` handlers, and clarified the extension docs around provider-payload rewrites and what `ctx.getSystemPrompt()` does and does not report ([#3539](https://github.com/badlogic/pi-mono/issues/3539))
|
||||
- Fixed extension session-replacement flows so `ctx.newSession()`, `ctx.fork()`, `ctx.switchSession()`, and imported-session replacements fully rebind before post-switch work runs, added `withSession` replacement callbacks with fresh `ReplacedSessionContext` helpers, and make stale pre-replacement `pi` / `ctx` session-bound accesses throw instead of silently targeting the wrong session ([#2860](https://github.com/badlogic/pi-mono/issues/2860))
|
||||
- Fixed `models.json` built-in provider overrides to accept `headers` without requiring `baseUrl`, so request-header-only overrides now load and apply correctly ([#3538](https://github.com/badlogic/pi-mono/issues/3538))
|
||||
|
||||
|
||||
@@ -466,7 +466,8 @@ Fired after user submits prompt, before agent loop. Can inject a message and/or
|
||||
pi.on("before_agent_start", async (event, ctx) => {
|
||||
// event.prompt - user's prompt text
|
||||
// event.images - attached images (if any)
|
||||
// event.systemPrompt - current system prompt
|
||||
// event.systemPrompt - current chained system prompt for this handler
|
||||
// (includes changes from earlier before_agent_start handlers)
|
||||
// event.systemPromptOptions - structured options used to build the system prompt
|
||||
// .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)
|
||||
// .selectedTools - tools currently active in the prompt
|
||||
@@ -492,6 +493,8 @@ pi.on("before_agent_start", async (event, ctx) => {
|
||||
|
||||
The `systemPromptOptions` field gives extensions access to the same structured data Pi uses to build the system prompt. This lets you inspect what Pi has loaded — custom prompts, guidelines, tool snippets, context files, skills — without re-discovering resources or re-parsing flags. Use it when your extension needs to make deep, informed changes to the system prompt while respecting user-provided configuration.
|
||||
|
||||
Inside `before_agent_start`, `event.systemPrompt` and `ctx.getSystemPrompt()` both reflect the chained system prompt as of the current handler. Later `before_agent_start` handlers can still modify it again.
|
||||
|
||||
#### agent_start / agent_end
|
||||
|
||||
Fired once per user prompt.
|
||||
@@ -580,6 +583,8 @@ pi.on("context", async (event, ctx) => {
|
||||
|
||||
Fired after the provider-specific payload is built, right before the request is sent. Handlers run in extension load order. Returning `undefined` keeps the payload unchanged. Returning any other value replaces the payload for later handlers and for the actual request.
|
||||
|
||||
This hook can rewrite provider-level system instructions or remove them entirely. Those payload-level changes are not reflected by `ctx.getSystemPrompt()`, which reports Pi's system prompt string rather than the final serialized provider payload.
|
||||
|
||||
```typescript
|
||||
pi.on("before_provider_request", (event, ctx) => {
|
||||
console.log(JSON.stringify(event.payload, null, 2));
|
||||
@@ -918,7 +923,12 @@ ctx.compact({
|
||||
|
||||
### ctx.getSystemPrompt()
|
||||
|
||||
Returns the current effective system prompt. This includes any modifications made by `before_agent_start` handlers for the current turn.
|
||||
Returns Pi's current system prompt string.
|
||||
|
||||
- During `before_agent_start`, this reflects chained system-prompt changes made so far for the current turn.
|
||||
- It does not include later `context` message mutations.
|
||||
- It does not include `before_provider_request` payload rewrites.
|
||||
- If later-loaded extensions run after yours, they can still change what is ultimately sent.
|
||||
|
||||
```typescript
|
||||
pi.on("before_agent_start", (event, ctx) => {
|
||||
|
||||
@@ -877,9 +877,16 @@ export class ExtensionRunner {
|
||||
systemPrompt: string,
|
||||
systemPromptOptions: BuildSystemPromptOptions,
|
||||
): Promise<BeforeAgentStartCombinedResult | undefined> {
|
||||
const ctx = this.createContext();
|
||||
const messages: NonNullable<BeforeAgentStartEventResult["message"]>[] = [];
|
||||
let currentSystemPrompt = systemPrompt;
|
||||
const ctx = Object.defineProperties(
|
||||
{},
|
||||
Object.getOwnPropertyDescriptors(this.createContext()),
|
||||
) as ExtensionContext;
|
||||
ctx.getSystemPrompt = () => {
|
||||
this.assertActive();
|
||||
return currentSystemPrompt;
|
||||
};
|
||||
const messages: NonNullable<BeforeAgentStartEventResult["message"]>[] = [];
|
||||
let systemPromptModified = false;
|
||||
|
||||
for (const ext of this.extensions) {
|
||||
|
||||
@@ -562,6 +562,50 @@ describe("ExtensionRunner", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("before_agent_start", () => {
|
||||
it("keeps ctx.getSystemPrompt() in sync with chained system prompt updates", async () => {
|
||||
const extCode1 = `
|
||||
export default function(pi) {
|
||||
pi.on("before_agent_start", async (_event, ctx) => {
|
||||
return {
|
||||
systemPrompt: ctx.getSystemPrompt() + "\\nfirst",
|
||||
};
|
||||
});
|
||||
}
|
||||
`;
|
||||
const extCode2 = `
|
||||
export default function(pi) {
|
||||
pi.on("before_agent_start", async (_event, ctx) => {
|
||||
return {
|
||||
systemPrompt: ctx.getSystemPrompt() + "\\nsecond",
|
||||
};
|
||||
});
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(path.join(extensionsDir, "before-agent-start-1.ts"), extCode1);
|
||||
fs.writeFileSync(path.join(extensionsDir, "before-agent-start-2.ts"), extCode2);
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
expect(result.errors).toEqual([]);
|
||||
expect(result.extensions).toHaveLength(2);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const errors: string[] = [];
|
||||
runner.onError((error) => errors.push(error.error));
|
||||
runner.bindCore(extensionActions, extensionContextActions);
|
||||
|
||||
const chained = await runner.emitBeforeAgentStart("hello", undefined, "base", {
|
||||
cwd: tempDir,
|
||||
});
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
|
||||
expect(chained).toEqual({
|
||||
messages: undefined,
|
||||
systemPrompt: "base\nfirst\nsecond",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("tool_result chaining", () => {
|
||||
it("chains content modifications across handlers", async () => {
|
||||
const extCode1 = `
|
||||
|
||||
Reference in New Issue
Block a user