@madgagarin/pi-agentrouter 1.2.0 → 1.3.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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/index.ts +107 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -120,6 +120,9 @@ AgentRouter performs client fingerprint verification. Raw summarization requests
120
120
  #### Q: Does the 2.5s pacing delay affect local or other cloud models?
121
121
  No. The pacing logic specifically filters for AgentRouter endpoints (`isAgentRouter`). Native OpenAI, Anthropic, Gemini, or local models run at full speed without delay.
122
122
 
123
+ #### Q: How to use custom subagents with AgentRouter?
124
+ AgentRouter strictly verifies client authenticity (`pi-code` / `claude-code` prompt signature). If you define custom subagents in extensions like `pi-subagents`, make sure to specify `systemPromptMode: append` in your agent definition frontmatter so the base Pi system prompt identity is preserved.
125
+
123
126
  ---
124
127
 
125
128
  ## 📄 License
package/index.ts CHANGED
@@ -90,7 +90,27 @@ export function fixPackagePriorityInSettings(): boolean {
90
90
  const initialConfig = loadConfig();
91
91
  let currentApiKey = normalizeApiKey(process.env.AGENTROUTER_API_KEY || initialConfig.apiKey || "");
92
92
  let minIntervalMs = initialConfig.minIntervalMs ?? 2500;
93
- let lastRequestEndTime = 0;
93
+ const PACING_FILE = path.join(process.env.HOME || "", ".pi/agent/.agentrouter-pacing");
94
+
95
+ export function getLastRequestEndTime(): number {
96
+ try {
97
+ if (fs.existsSync(PACING_FILE)) {
98
+ const val = parseInt(fs.readFileSync(PACING_FILE, "utf-8").trim(), 10);
99
+ if (!isNaN(val)) return val;
100
+ }
101
+ } catch {}
102
+ return 0;
103
+ }
104
+
105
+ export function setLastRequestEndTime(ts: number): void {
106
+ try {
107
+ const dir = path.dirname(PACING_FILE);
108
+ if (!fs.existsSync(dir)) {
109
+ fs.mkdirSync(dir, { recursive: true });
110
+ }
111
+ fs.writeFileSync(PACING_FILE, String(ts), "utf-8");
112
+ } catch {}
113
+ }
94
114
 
95
115
  export function isAgentRouter(providerName?: string, baseUrl?: string): boolean {
96
116
  if (providerName && providerName.toLowerCase().includes("agentrouter")) return true;
@@ -98,6 +118,53 @@ export function isAgentRouter(providerName?: string, baseUrl?: string): boolean
98
118
  return false;
99
119
  }
100
120
 
121
+ export const CANONICAL_PI_HEADER =
122
+ "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.";
123
+
124
+ /**
125
+ * Enforces that the canonical pi-code system prompt signature is strictly at index 0.
126
+ * If another plugin or wrapper prepended text before the canonical header, it reorders
127
+ * the header to the very top and shifts the injected prefix right after it.
128
+ * If the header is missing entirely (e.g. from replace mode), it prepends the canonical header.
129
+ * This guarantees both WAF client authentication and stable prompt cache prefix matching.
130
+ */
131
+ export function enforceCanonicalRootPrompt(systemPrompt: string | any[] | undefined): string | any[] {
132
+ if (!systemPrompt) {
133
+ return CANONICAL_PI_HEADER;
134
+ }
135
+
136
+ if (typeof systemPrompt === "string") {
137
+ const text = systemPrompt.trim();
138
+ const piHeaderRegex = /(?:You are [^\n\r]*operating inside pi[^\n\r]*\n?|You are (?:pi|Pi)[^\n\r]*\n?)/i;
139
+
140
+ if (piHeaderRegex.test(text)) {
141
+ const match = text.match(piHeaderRegex);
142
+ if (match && match.index !== undefined && match.index > 0) {
143
+ const header = match[0].trim();
144
+ const prefix = text.slice(0, match.index).trim();
145
+ const rest = text.slice(match.index + match[0].length).trim();
146
+ return `${header}\n\n${prefix}${rest ? "\n\n" + rest : ""}`;
147
+ }
148
+ return text;
149
+ } else {
150
+ return `${CANONICAL_PI_HEADER}\n\n${text}`;
151
+ }
152
+ }
153
+
154
+ if (Array.isArray(systemPrompt)) {
155
+ if (systemPrompt.length === 0) {
156
+ return [{ type: "text", text: CANONICAL_PI_HEADER }];
157
+ }
158
+ const firstBlock = systemPrompt[0];
159
+ if (firstBlock && typeof firstBlock.text === "string") {
160
+ firstBlock.text = enforceCanonicalRootPrompt(firstBlock.text) as string;
161
+ }
162
+ return systemPrompt;
163
+ }
164
+
165
+ return systemPrompt;
166
+ }
167
+
101
168
  export default function (pi: ExtensionAPI) {
102
169
  function registerAgentRouterProviders(apiKey: string): void {
103
170
  pi.registerProvider("agentrouter-openai", {
@@ -129,8 +196,10 @@ export default function (pi: ExtensionAPI) {
129
196
  baseUrl: "https://agentrouter.org",
130
197
  apiKey,
131
198
  api: "anthropic-messages",
199
+
132
200
  compat: {
133
201
  forceAdaptiveThinking: true,
202
+ sendSessionAffinityHeaders: true,
134
203
  },
135
204
  models: [
136
205
  {
@@ -143,6 +212,7 @@ export default function (pi: ExtensionAPI) {
143
212
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
144
213
  compat: {
145
214
  forceAdaptiveThinking: true,
215
+ sendSessionAffinityHeaders: true,
146
216
  },
147
217
  },
148
218
  {
@@ -155,6 +225,7 @@ export default function (pi: ExtensionAPI) {
155
225
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
156
226
  compat: {
157
227
  forceAdaptiveThinking: true,
228
+ sendSessionAffinityHeaders: true,
158
229
  },
159
230
  },
160
231
  ],
@@ -171,9 +242,37 @@ export default function (pi: ExtensionAPI) {
171
242
  }
172
243
  }
173
244
 
245
+ // Enforce root system prompt signature and prompt prefix stability at the lowest transport level
246
+ pi.on("before_provider_request", async (event, ctx) => {
247
+ const provider = ((event as any)?.model?.provider || ctx?.model?.provider || "").toLowerCase();
248
+ const baseUrl = ((event as any)?.model?.baseUrl || (ctx?.model as any)?.baseUrl || "");
249
+
250
+ if (isAgentRouter(provider, baseUrl)) {
251
+ const payload = event.payload;
252
+ if (payload) {
253
+ // 1. Enforce canonical root system prompt in payload.system (Anthropic format)
254
+ if (payload.system !== undefined) {
255
+ payload.system = enforceCanonicalRootPrompt(payload.system);
256
+ }
257
+
258
+ // 2. Enforce canonical root system prompt in payload.messages[0] (OpenAI format)
259
+ if (Array.isArray(payload.messages) && payload.messages.length > 0) {
260
+ const firstMsg = payload.messages[0];
261
+ if (firstMsg && firstMsg.role === "system") {
262
+ if (typeof firstMsg.content === "string") {
263
+ firstMsg.content = enforceCanonicalRootPrompt(firstMsg.content);
264
+ } else if (Array.isArray(firstMsg.content)) {
265
+ firstMsg.content = enforceCanonicalRootPrompt(firstMsg.content);
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+ });
272
+
174
273
  pi.on("session_start", async (_event, ctx) => {
175
274
  updatePromptRewriteEnvForModel(ctx.model);
176
- lastRequestEndTime = Date.now();
275
+ setLastRequestEndTime(Date.now());
177
276
 
178
277
  const order = getPackageOrderState();
179
278
  if (order.needsFix && ctx.hasUI && typeof (ctx.ui as any).confirm === "function") {
@@ -209,10 +308,11 @@ export default function (pi: ExtensionAPI) {
209
308
  return;
210
309
  }
211
310
 
311
+ const lastEnd = getLastRequestEndTime();
212
312
  const now = Date.now();
213
- const elapsed = now - lastRequestEndTime;
313
+ const elapsed = now - lastEnd;
214
314
 
215
- if (lastRequestEndTime > 0 && elapsed < minIntervalMs) {
315
+ if (lastEnd > 0 && elapsed < minIntervalMs) {
216
316
  const waitMs = minIntervalMs - elapsed;
217
317
  await new Promise((resolve) => setTimeout(resolve, waitMs));
218
318
  }
@@ -221,14 +321,14 @@ export default function (pi: ExtensionAPI) {
221
321
  pi.on("turn_end", async (_event, ctx) => {
222
322
  const model = ctx.model;
223
323
  if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
224
- lastRequestEndTime = Date.now();
324
+ setLastRequestEndTime(Date.now());
225
325
  }
226
326
  });
227
327
 
228
328
  pi.on("agent_end", async (_event, ctx) => {
229
329
  const model = ctx.model;
230
330
  if (isAgentRouter(model?.provider, (model as any)?.baseUrl)) {
231
- lastRequestEndTime = Date.now();
331
+ setLastRequestEndTime(Date.now());
232
332
  }
233
333
  });
234
334
 
@@ -276,6 +376,7 @@ export default function (pi: ExtensionAPI) {
276
376
  signal,
277
377
  }
278
378
  );
379
+ setLastRequestEndTime(Date.now());
279
380
 
280
381
  const summary = response.content
281
382
  .filter((c: any): c is { type: "text"; text: string } => c.type === "text")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madgagarin/pi-agentrouter",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Pi coding agent extension for AgentRouter (GPT-5.6 Sol & Claude Opus 5) with rate-limit pacing and caching.",
5
5
  "publishConfig": {
6
6
  "access": "public"