mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
chore: Pi 开发依赖跟随最新版并去掉测试里的固定版本号
- pi-hermes-memory / pi-notify / pi-permission-system / pi-subagents 的 @earendil-works/* devDependencies 改为 latest,pi-rtk-optimizer 放开 peer 范围;各子包锁文件刷新到 Pi 1.3.0,测试改为验证当前宿主 API。 - 新增 scripts/update-pi-devdeps.sh,统一刷新根与子包锁文件中的 Pi 版本。 - pi-subagents 适配 Pi 1.3:mention clone 改走 systemPromptOverride、 BOM frontmatter、e2e 读取规范化后的 system 消息与工具列表。 - 根测试不再写死 ssh2 / jiti / better-sqlite3 / context-mode 版本号, 改为精确版本格式与锁文件一致性断言;新增 latest 约束测试。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
ba164896d2
commit
b817d66a11
@@ -124,15 +124,15 @@ describe("disableInContent", () => {
|
||||
expect(loaderSeesDisabled(content)).toBe(true);
|
||||
});
|
||||
|
||||
it("reports no-frontmatter for a BOM-prefixed file, matching what the loader sees", () => {
|
||||
// The parser doesn't accept a BOM either — it reports an empty frontmatter
|
||||
// and treats the whole file as body. So refusing here is agreement with the
|
||||
// read side, not a gap: inserting the key would have no effect on loading.
|
||||
it("disables a BOM-prefixed file, matching what the loader sees", () => {
|
||||
// Pi ≥1.3's parser skips a leading BOM, so the block behind it is real
|
||||
// frontmatter — refusing here would leave a file the loader reads enabled
|
||||
// while reporting "nothing to do". The edit keeps the BOM in place.
|
||||
const src = "---\ndescription: Scout\n---\n\nBody.\n";
|
||||
expect(parseFrontmatter<Record<string, unknown>>(src).frontmatter).toEqual({});
|
||||
const { content, outcome } = disableInContent(src);
|
||||
expect(outcome).toBe("no-frontmatter");
|
||||
expect(content).toBe(src);
|
||||
expect(outcome).toBe("disabled");
|
||||
expect(content.startsWith("")).toBe(true);
|
||||
expect(loaderSeesDisabled(content)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -112,20 +112,19 @@ describe("subagent usage reaches the parent session's stats (real pi)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves the context-window percentage alone", async () => {
|
||||
// pi derives context usage from assistant messages only. If that ever
|
||||
// changed, a delegating session would look like it was filling its context
|
||||
// with work that happened somewhere else entirely — and users would compact
|
||||
// for no reason.
|
||||
it("counts only the result's own text toward context, not its reported usage", async () => {
|
||||
// Since Pi 1.3 the percentage reflects the transcript's real contents, so
|
||||
// the message's own few tokens do register. What must not register is the
|
||||
// reported usage: 150k delegated tokens on a 200k window would read as
|
||||
// ~75% occupied, and a delegating session would look like it was filling
|
||||
// its context with work that happened somewhere else entirely.
|
||||
const session = await realSession();
|
||||
try {
|
||||
const before = session.getSessionStats().contextUsage?.percent ?? null;
|
||||
|
||||
const pool = new PendingUsagePool();
|
||||
pool.add({ input: 150_000, output: 400, cacheWrite: 100, cost: 1.5 });
|
||||
session.sessionManager.appendMessage(toolResultCarrying(pool.drain()) as any);
|
||||
|
||||
expect(session.getSessionStats().contextUsage?.percent ?? null).toBe(before);
|
||||
expect(session.getSessionStats().contextUsage?.percent ?? 0).toBeLessThan(1);
|
||||
} finally {
|
||||
session.dispose?.();
|
||||
}
|
||||
|
||||
@@ -195,6 +195,46 @@ function resolveReply(
|
||||
return typeof reply === "function" ? (reply as (c: Context) => FauxReply)(ctx) : reply;
|
||||
}
|
||||
|
||||
/**
|
||||
* Names of the tools the session offered the model for this call. Pi ≥1.3
|
||||
* normalizes `Context.tools` into the transcript's system messages
|
||||
* (`toolsAdded`/`toolsRemoved`) before the provider sees the request, so read
|
||||
* both shapes.
|
||||
*/
|
||||
export function contextToolNames(context: Context): string[] {
|
||||
const names = new Set<string>((context.tools ?? []).map((t) => t.name));
|
||||
for (const m of context.messages) {
|
||||
if (m.role !== "system") continue;
|
||||
for (const t of m.toolsAdded ?? []) names.add(t.name);
|
||||
for (const t of m.toolsRemoved ?? []) names.delete(t.name);
|
||||
}
|
||||
return [...names];
|
||||
}
|
||||
|
||||
/**
|
||||
* The system-prompt text the session offered for this call. Pi ≥1.3 normalizes
|
||||
* `Context.systemPrompt` into the transcript's system messages, so read both
|
||||
* shapes: `context.systemPrompt` plus every system message's `content` and
|
||||
* named `sections` values.
|
||||
*/
|
||||
export function contextSystemPrompt(context: Context): string {
|
||||
const parts: string[] = [];
|
||||
if (context.systemPrompt) parts.push(context.systemPrompt);
|
||||
for (const m of context.messages) {
|
||||
if (m.role !== "system") continue;
|
||||
const content = m.content;
|
||||
if (typeof content === "string") {
|
||||
parts.push(content);
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const b of content) if (b.type === "text" && b.text) parts.push(b.text);
|
||||
}
|
||||
for (const v of Object.values(m.sections ?? {})) {
|
||||
if (typeof v === "string") parts.push(v);
|
||||
}
|
||||
}
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* The common single-spawn flow as a responder. Routes by inspecting the calling
|
||||
* session's own context:
|
||||
@@ -210,7 +250,7 @@ export function routeBySession(routes: {
|
||||
subagent: FauxReply | ((ctx: Context) => FauxReply);
|
||||
}): FauxResponder {
|
||||
return (context) => {
|
||||
const isParent = (context.tools ?? []).some((t) => t.name === "Agent");
|
||||
const isParent = contextToolNames(context).includes("Agent");
|
||||
if (!isParent) return resolveReply(routes.subagent, context);
|
||||
const spawned = context.messages.some(
|
||||
(m) => m.role === "toolResult" && (m as { toolName?: string }).toolName === "Agent",
|
||||
|
||||
@@ -17,10 +17,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Hoisted: vi.mock's factory is lifted above the imports, so it cannot close
|
||||
// over ordinary top-level consts.
|
||||
const { buildSessionContext, createAgentSession, inMemory } = vi.hoisted(() => ({
|
||||
const { buildSessionContext, createAgentSession, inMemory, loaderOptions } = vi.hoisted(() => ({
|
||||
buildSessionContext: vi.fn(),
|
||||
createAgentSession: vi.fn(),
|
||||
inMemory: vi.fn(),
|
||||
// The clone's prompt reaches the session through the loader's
|
||||
// systemPromptOverride (Pi 1.3 made AgentState.systemPrompt read-only), so
|
||||
// capture what runMentionClone hands DefaultResourceLoader.
|
||||
loaderOptions: { current: undefined as Record<string, unknown> | undefined },
|
||||
}));
|
||||
|
||||
vi.mock("@earendil-works/pi-coding-agent", async () => {
|
||||
@@ -29,6 +33,12 @@ vi.mock("@earendil-works/pi-coding-agent", async () => {
|
||||
...actual,
|
||||
buildSessionContext,
|
||||
createAgentSession,
|
||||
DefaultResourceLoader: class extends actual.DefaultResourceLoader {
|
||||
constructor(options?: Record<string, unknown>) {
|
||||
loaderOptions.current = options;
|
||||
super(options);
|
||||
}
|
||||
},
|
||||
SessionManager: { ...actual.SessionManager, inMemory },
|
||||
};
|
||||
});
|
||||
@@ -44,6 +54,7 @@ const CONVERSATION = [
|
||||
|
||||
beforeEach(() => {
|
||||
createAgentSession.mockReset();
|
||||
loaderOptions.current = undefined;
|
||||
inMemory.mockReset();
|
||||
inMemory.mockReturnValue({ kind: "in-memory-session-manager" } as any);
|
||||
buildSessionContext.mockReset();
|
||||
@@ -104,6 +115,13 @@ function cloneSession(turn?: (tool: any) => Promise<void> | void) {
|
||||
dispose: vi.fn(),
|
||||
} as any;
|
||||
createAgentSession.mockImplementation(async (opts: any) => {
|
||||
// What the real wiring does: the loader's systemPromptOverride rewrites the
|
||||
// rebuilt base before the session's state gets it.
|
||||
const override = loaderOptions.current?.systemPromptOverride as
|
||||
| ((base: string | undefined) => string | undefined)
|
||||
| undefined;
|
||||
const overridden = override?.(session.agent.state.systemPrompt);
|
||||
if (overridden !== undefined) session.agent.state.systemPrompt = overridden;
|
||||
const tools = visibleTools(opts);
|
||||
session.prompt.mockImplementation(async () => {
|
||||
// No tool, no tool call: the model can only answer in prose.
|
||||
|
||||
@@ -26,6 +26,7 @@ import { loadCustomAgents } from "../src/custom-agents.js";
|
||||
import { encodeCwd } from "../src/output-file.js";
|
||||
import {
|
||||
agentCall,
|
||||
contextToolNames,
|
||||
type FauxReply,
|
||||
type PrintModeRun,
|
||||
runPrintMode,
|
||||
@@ -101,7 +102,7 @@ describe("nested delegation e2e (real pi-mono, faux model)", () => {
|
||||
|
||||
const respond = (context: Context): FauxReply => {
|
||||
const text = firstUserText(context);
|
||||
const names = (context.tools ?? []).map((t) => t.name);
|
||||
const names = contextToolNames(context);
|
||||
|
||||
// Leaf: no nested tools (it never opted in) — just answer.
|
||||
if (text.includes("Do the leaf work")) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { registerAgents } from "../src/agent-types.js";
|
||||
import { loadCustomAgents } from "../src/custom-agents.js";
|
||||
import {
|
||||
agentCall,
|
||||
contextToolNames,
|
||||
type FauxResponder,
|
||||
type PrintModeRun,
|
||||
runPrintMode,
|
||||
@@ -47,7 +48,7 @@ function userPrompt(ctx: Context): string {
|
||||
}
|
||||
|
||||
function tools(ctx: Context): string[] {
|
||||
return (ctx.tools ?? []).map((tool) => tool.name);
|
||||
return contextToolNames(ctx);
|
||||
}
|
||||
|
||||
function toolResults(ctx: Context, name: string): string[] {
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
agentCall,
|
||||
agentToolCalls,
|
||||
agentToolResults,
|
||||
contextSystemPrompt,
|
||||
contextToolNames,
|
||||
conversationText,
|
||||
invokedToolNames,
|
||||
type PrintModeRun,
|
||||
@@ -92,7 +94,7 @@ describe.skipIf(LIVE)("subagents print-mode e2e (scripted faux, real pi-mono)",
|
||||
// finishes → the child's own model turn actually runs (≥3 calls).
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
const respond = async (ctx: Context) => {
|
||||
const isParent = (ctx.tools ?? []).some((t) => t.name === "Agent");
|
||||
const isParent = contextToolNames(ctx).includes("Agent");
|
||||
if (!isParent) {
|
||||
await sleep(80); // child takes long enough that a non-held parent exits first
|
||||
return "CHILD_BG_RAN";
|
||||
@@ -151,7 +153,7 @@ describe.skipIf(LIVE)("subagents print-mode e2e (scripted faux, real pi-mono)",
|
||||
parentFinal: "Reported.",
|
||||
// The child reflects whether the frontmatter body reached its own prompt.
|
||||
subagent: (ctx: Context) =>
|
||||
`child saw: ${ctx.systemPrompt?.includes(MARKER) ? MARKER : "MISSING"}`,
|
||||
`child saw: ${contextSystemPrompt(ctx).includes(MARKER) ? MARKER : "MISSING"}`,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -185,7 +187,7 @@ describe.skipIf(LIVE)("subagents print-mode e2e (scripted faux, real pi-mono)",
|
||||
}),
|
||||
parentFinal: "Reported.",
|
||||
subagent: (ctx: Context) =>
|
||||
`child saw: ${ctx.systemPrompt?.includes(MARKER) ? MARKER : "MISSING"}`,
|
||||
`child saw: ${contextSystemPrompt(ctx).includes(MARKER) ? MARKER : "MISSING"}`,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user