enhance pi configuration and skills
This commit is contained in:
parent
623512cbf5
commit
ab0a86a707
18 changed files with 2000 additions and 159 deletions
21
pi/extensions/openai-server-compaction/LICENSE.md
Normal file
21
pi/extensions/openai-server-compaction/LICENSE.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Alexis Gallagher
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
81
pi/extensions/openai-server-compaction/README.md
Normal file
81
pi/extensions/openai-server-compaction/README.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# OpenAI server compaction
|
||||
|
||||
A vendored Pi extension that uses OpenAI's Responses compaction protocol for GPT
|
||||
models while preserving Pi's session, tree, and fallback behavior.
|
||||
|
||||
This fork is based on
|
||||
[`algal/pi-openai-server-compaction`](https://github.com/algal/pi-openai-server-compaction)
|
||||
and retains its MIT license. It is adapted for this dotfiles repository and Pi
|
||||
0.82.
|
||||
|
||||
## Scope
|
||||
|
||||
Remote compaction is intentionally limited to GPT models on these native Pi
|
||||
providers:
|
||||
|
||||
- `openai/*` using `openai-responses`
|
||||
- `openai-codex/*` using `openai-codex-responses`
|
||||
|
||||
Other models and providers are untouched and continue using Pi's default
|
||||
compaction.
|
||||
|
||||
## Compaction behavior
|
||||
|
||||
Pi remains responsible for deciding when to compact, selecting the cut point,
|
||||
and writing the compaction entry. On `session_before_compact`, this extension:
|
||||
|
||||
1. asks OpenAI for an opaque `compaction` item through the Responses API;
|
||||
2. generates a portable text summary in parallel;
|
||||
3. stores the opaque replacement history in
|
||||
`CompactionEntry.details.remoteCompaction`; and
|
||||
4. replays that history on later requests to the exact same provider/API/model.
|
||||
|
||||
If remote compaction fails but the portable summary succeeds, Pi uses that text
|
||||
summary. If neither extension path succeeds, the handler returns control to
|
||||
Pi's default compactor.
|
||||
|
||||
The opaque artifact is model-specific. Switching models uses Pi's portable text
|
||||
summary; switching back reconstructs the matching artifact from session JSONL.
|
||||
|
||||
## Pi 0.82 adaptation
|
||||
|
||||
Unlike upstream, this fork does not override Pi's OpenAI provider or install a
|
||||
custom WebSocket transport. It uses Pi 0.82's native HTTP Responses transport.
|
||||
This avoids the upstream WebSocket partial-rendering issue and removes the
|
||||
runtime `ws` dependency.
|
||||
|
||||
Because Pi's full replay payload is not safe to combine with
|
||||
`previous_response_id`, this fork disables that optimization. It also leaves
|
||||
normal pre-compaction requests unchanged instead of enabling OpenAI's automatic
|
||||
`context_management`, whose compaction stream events Pi 0.82 does not natively
|
||||
persist. Pi triggers compaction normally; post-compaction requests replay the
|
||||
opaque artifact explicitly.
|
||||
|
||||
## Data handling
|
||||
|
||||
Conversation context is sent to OpenAI during compaction with `store: false`,
|
||||
and returned encrypted artifacts are stored in Pi's local session JSONL. The
|
||||
artifacts are not human-readable. OpenAI's normal API data-handling and abuse
|
||||
monitoring policies still apply.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is read from:
|
||||
|
||||
- `~/.pi/agent/openai-server-compaction.json`
|
||||
- `.pi/openai-server-compaction.json` (takes precedence)
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"notify": false
|
||||
}
|
||||
```
|
||||
|
||||
Environment overrides:
|
||||
|
||||
- `PI_OPENAI_SERVER_COMPACTION_ENABLED`
|
||||
- `PI_OPENAI_SERVER_COMPACTION_NOTIFY`
|
||||
|
||||
Set `PI_OPENAI_SERVER_COMPACTION_ENABLED=0` for a quick rollback, or start Pi
|
||||
with `--no-extensions` to bypass all extensions.
|
||||
59
pi/extensions/openai-server-compaction/config.ts
Normal file
59
pi/extensions/openai-server-compaction/config.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Configuration loading for the extension.
|
||||
*
|
||||
* Reads global/project JSON config files plus environment overrides and exposes
|
||||
* a normalized, fully-populated runtime config object.
|
||||
*/
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
export type JsonRecord = Record<string, unknown>;
|
||||
|
||||
export type ExtensionConfig = {
|
||||
enabled?: boolean;
|
||||
notify?: boolean;
|
||||
};
|
||||
|
||||
export function isRecord(value: unknown): value is JsonRecord {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readJsonFile(path: string): JsonRecord | undefined {
|
||||
if (!existsSync(path)) return undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
||||
return isRecord(parsed) ? parsed : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function toBoolean(value: unknown): boolean | undefined {
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number") return value !== 0;
|
||||
if (typeof value !== "string") return undefined;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
||||
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function loadConfig(cwd: string): Required<ExtensionConfig> {
|
||||
const globalPath = join(homedir(), ".pi", "agent", "openai-server-compaction.json");
|
||||
const projectPath = join(cwd, ".pi", "openai-server-compaction.json");
|
||||
const globalCfg = readJsonFile(globalPath) ?? {};
|
||||
const projectCfg = readJsonFile(projectPath) ?? {};
|
||||
const merged = { ...globalCfg, ...projectCfg };
|
||||
|
||||
return {
|
||||
enabled:
|
||||
toBoolean(process.env.PI_OPENAI_SERVER_COMPACTION_ENABLED) ??
|
||||
toBoolean(merged.enabled) ??
|
||||
true,
|
||||
notify:
|
||||
toBoolean(process.env.PI_OPENAI_SERVER_COMPACTION_NOTIFY) ??
|
||||
toBoolean(merged.notify) ??
|
||||
false,
|
||||
};
|
||||
}
|
||||
348
pi/extensions/openai-server-compaction/index.ts
Normal file
348
pi/extensions/openai-server-compaction/index.ts
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
/**
|
||||
* Main extension entrypoint.
|
||||
*
|
||||
* Wires together request patching, remote compaction, runtime state
|
||||
* reconstruction, session lifecycle cleanup, and Pi-native HTTP request patching.
|
||||
*/
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||
import { isRecord, loadConfig } from "./config.ts";
|
||||
import {
|
||||
applyRemoteHistoryPayloadPatch,
|
||||
extractResponsesReasoningConfig,
|
||||
extractResponsesTextConfig,
|
||||
isOpenAICodexResponsesModel,
|
||||
looksLikeResponsesPayload,
|
||||
messageMatchesModel,
|
||||
modelKey,
|
||||
supportsRemoteCompactionModel,
|
||||
thinkingLevelToResponsesReasoning,
|
||||
} from "./openai.ts";
|
||||
import {
|
||||
buildCompactionSummaryText,
|
||||
buildRemoteCompactionDetails,
|
||||
buildToolsPayload,
|
||||
callRemoteCompactionEndpoint,
|
||||
generateBestEffortLocalSummary,
|
||||
messageToResponseItems,
|
||||
messagesToResponseItems,
|
||||
normalizeResponseItemsForPrompt,
|
||||
reconstructRemoteCompactionStateFromBranch,
|
||||
} from "./remote-compaction.ts";
|
||||
import {
|
||||
clearAllRuntimeState,
|
||||
clearRemoteCompactionState,
|
||||
clearResponsesRequestShapeState,
|
||||
getRemoteCompactionState,
|
||||
getResponsesRequestShapeState,
|
||||
setRemoteCompactionState,
|
||||
setResponsesRequestShapeState,
|
||||
} from "./state.ts";
|
||||
|
||||
type TargetModel = Parameters<typeof modelKey>[0];
|
||||
|
||||
type BranchEntry = {
|
||||
type: string;
|
||||
id: string;
|
||||
details?: unknown;
|
||||
message?: unknown;
|
||||
thinkingLevel?: unknown;
|
||||
};
|
||||
|
||||
type SessionContextLike = {
|
||||
sessionManager: {
|
||||
getSessionId(): string;
|
||||
getBranch(): BranchEntry[];
|
||||
};
|
||||
};
|
||||
|
||||
function getSessionId(ctx: SessionContextLike): string {
|
||||
return ctx.sessionManager.getSessionId();
|
||||
}
|
||||
|
||||
function getBranchMessages(branchEntries: BranchEntry[]): AgentMessage[] {
|
||||
return branchEntries.flatMap((entry) =>
|
||||
entry.type === "message" && entry.message ? [entry.message as AgentMessage] : [],
|
||||
);
|
||||
}
|
||||
|
||||
function getBranchThinkingLevel(branchEntries: BranchEntry[]): string | undefined {
|
||||
for (let index = branchEntries.length - 1; index >= 0; index--) {
|
||||
const entry = branchEntries[index];
|
||||
if (entry?.type !== "thinking_level_change") continue;
|
||||
return typeof entry.thinkingLevel === "string" ? entry.thinkingLevel : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function clearSessionRuntimeState(sessionId: string | undefined): void {
|
||||
clearRemoteCompactionState(sessionId);
|
||||
clearResponsesRequestShapeState(sessionId);
|
||||
}
|
||||
|
||||
function syncRemoteState(ctx: SessionContextLike): void {
|
||||
const sessionId = getSessionId(ctx);
|
||||
const branchEntries = ctx.sessionManager.getBranch() as Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
details?: unknown;
|
||||
message?: AgentMessage;
|
||||
}>;
|
||||
const state = reconstructRemoteCompactionStateFromBranch({ branchEntries });
|
||||
if (state) {
|
||||
setRemoteCompactionState(sessionId, state);
|
||||
} else {
|
||||
clearRemoteCompactionState(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
function getMatchingRemoteState(
|
||||
sessionId: string,
|
||||
model: TargetModel | undefined,
|
||||
): ReturnType<typeof getRemoteCompactionState> {
|
||||
if (!model) return undefined;
|
||||
const remoteState = getRemoteCompactionState(sessionId);
|
||||
return remoteState && remoteState.modelKey === modelKey(model) ? remoteState : undefined;
|
||||
}
|
||||
|
||||
function extendRemoteHistoryIfCompatible(params: {
|
||||
sessionId: string;
|
||||
model: TargetModel | undefined;
|
||||
message: AgentMessage;
|
||||
}): void {
|
||||
const remoteState = getMatchingRemoteState(params.sessionId, params.model);
|
||||
if (!remoteState || !params.model) return;
|
||||
if (params.message.role === "assistant" && !messageMatchesModel(params.message, params.model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = messageToResponseItems(params.message);
|
||||
if (items.length === 0) return;
|
||||
|
||||
setRemoteCompactionState(params.sessionId, {
|
||||
...remoteState,
|
||||
explicitHistory: [...remoteState.explicitHistory, ...items],
|
||||
});
|
||||
}
|
||||
|
||||
function maybeNotifyRequestFeatures(params: {
|
||||
notifiedModels: Set<string>;
|
||||
hasUI: boolean;
|
||||
notify: boolean;
|
||||
ui: { notify(message: string, level: "info" | "warning"): void };
|
||||
model: TargetModel;
|
||||
features: string[];
|
||||
}): void {
|
||||
if (!params.notify || !params.hasUI || params.features.length === 0) return;
|
||||
|
||||
const key = `${String(params.model.provider)}/${String(params.model.id)}`;
|
||||
const noticeKey = `${key}:${params.features.join(",")}`;
|
||||
if (params.notifiedModels.has(noticeKey)) return;
|
||||
|
||||
params.notifiedModels.add(noticeKey);
|
||||
params.ui.notify(`OpenAI compaction active for ${key} (${params.features.join(", ")})`, "info");
|
||||
}
|
||||
|
||||
export default function openaiServerCompactionExtension(pi: ExtensionAPI) {
|
||||
const notifiedModels = new Set<string>();
|
||||
|
||||
pi.on("session_start", (_event, ctx) => {
|
||||
const sessionId = getSessionId(ctx);
|
||||
clearResponsesRequestShapeState(sessionId);
|
||||
syncRemoteState(ctx);
|
||||
});
|
||||
|
||||
const clearBeforeSessionChange = (_event: unknown, ctx: SessionContextLike): void => {
|
||||
clearSessionRuntimeState(getSessionId(ctx));
|
||||
};
|
||||
pi.on("session_before_switch", clearBeforeSessionChange);
|
||||
pi.on("session_before_fork", clearBeforeSessionChange);
|
||||
pi.on("session_before_tree", clearBeforeSessionChange);
|
||||
|
||||
const syncAfterSessionChange = (_event: unknown, ctx: SessionContextLike): void => {
|
||||
syncRemoteState(ctx);
|
||||
};
|
||||
pi.on("session_tree", syncAfterSessionChange);
|
||||
pi.on("session_compact", syncAfterSessionChange);
|
||||
|
||||
pi.on("model_select", (_event, ctx) => {
|
||||
clearResponsesRequestShapeState(getSessionId(ctx));
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", () => {
|
||||
clearAllRuntimeState();
|
||||
});
|
||||
|
||||
pi.on("session_before_compact", async (event, ctx) => {
|
||||
const cfg = loadConfig(ctx.cwd);
|
||||
const model = ctx.model;
|
||||
if (!cfg.enabled || !model || !supportsRemoteCompactionModel(model)) return undefined;
|
||||
|
||||
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
||||
if (!auth.ok || !auth.apiKey) return undefined;
|
||||
|
||||
const tools = buildToolsPayload(pi.getAllTools(), pi.getActiveTools());
|
||||
const sessionId = getSessionId(ctx);
|
||||
const branchEntries = event.branchEntries as BranchEntry[];
|
||||
const remoteState = getMatchingRemoteState(sessionId, model);
|
||||
const observedRequestShape = getResponsesRequestShapeState(sessionId);
|
||||
const fullBranchMessages = getBranchMessages(branchEntries);
|
||||
const responseItems = remoteState
|
||||
? remoteState.explicitHistory
|
||||
: messagesToResponseItems(fullBranchMessages);
|
||||
const promptResponseItems = normalizeResponseItemsForPrompt(responseItems, model);
|
||||
const thinkingLevel = pi.getThinkingLevel();
|
||||
const fallbackReasoning = model.reasoning
|
||||
? thinkingLevelToResponsesReasoning(thinkingLevel ?? getBranchThinkingLevel(branchEntries))
|
||||
: undefined;
|
||||
const reasoning = observedRequestShape?.reasoning ?? fallbackReasoning;
|
||||
const text = observedRequestShape?.text;
|
||||
|
||||
const [localResult, remoteResult] = await Promise.allSettled([
|
||||
generateBestEffortLocalSummary({
|
||||
preparation: event.preparation,
|
||||
messages: fullBranchMessages,
|
||||
model,
|
||||
apiKey: auth.apiKey,
|
||||
headers: auth.headers,
|
||||
customInstructions: event.customInstructions,
|
||||
signal: event.signal,
|
||||
thinkingLevel,
|
||||
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
||||
tokensBefore: event.preparation.tokensBefore,
|
||||
}),
|
||||
callRemoteCompactionEndpoint({
|
||||
model,
|
||||
apiKey: auth.apiKey,
|
||||
headers: auth.headers,
|
||||
sessionId,
|
||||
input: promptResponseItems,
|
||||
instructions: ctx.getSystemPrompt(),
|
||||
tools,
|
||||
parallelToolCalls: true,
|
||||
reasoning,
|
||||
text,
|
||||
signal: event.signal,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (remoteResult.status !== "fulfilled") {
|
||||
if (localResult.status === "fulfilled") {
|
||||
return { compaction: localResult.value };
|
||||
}
|
||||
if (!event.signal.aborted && ctx.hasUI) {
|
||||
const message =
|
||||
remoteResult.reason instanceof Error
|
||||
? remoteResult.reason.message
|
||||
: String(remoteResult.reason);
|
||||
ctx.ui.notify(
|
||||
`OpenAI remote compaction failed; falling back to default compaction. ${message}`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const remoteDetails = buildRemoteCompactionDetails(
|
||||
model,
|
||||
remoteResult.value.output,
|
||||
remoteResult.value.usage,
|
||||
);
|
||||
const localSummary =
|
||||
localResult.status === "fulfilled"
|
||||
? localResult.value
|
||||
: {
|
||||
summary: buildCompactionSummaryText(model),
|
||||
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
||||
tokensBefore: event.preparation.tokensBefore,
|
||||
};
|
||||
|
||||
return {
|
||||
compaction: {
|
||||
summary: localSummary.summary,
|
||||
firstKeptEntryId: localSummary.firstKeptEntryId,
|
||||
tokensBefore: localSummary.tokensBefore,
|
||||
details: {
|
||||
...(localSummary.details !== undefined
|
||||
? { localSummaryDetails: localSummary.details }
|
||||
: {}),
|
||||
remoteCompaction: remoteDetails,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
pi.on("message_end", (event, ctx) => {
|
||||
const sessionId = getSessionId(ctx);
|
||||
const model = ctx.model;
|
||||
|
||||
extendRemoteHistoryIfCompatible({
|
||||
sessionId,
|
||||
model,
|
||||
message: event.message,
|
||||
});
|
||||
});
|
||||
|
||||
pi.on("before_provider_request", (event, ctx) => {
|
||||
const cfg = loadConfig(ctx.cwd);
|
||||
if (!cfg.enabled) return undefined;
|
||||
|
||||
const model = ctx.model;
|
||||
if (
|
||||
!model ||
|
||||
!supportsRemoteCompactionModel(model) ||
|
||||
!isRecord(event.payload) ||
|
||||
!looksLikeResponsesPayload(event.payload)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sessionId = getSessionId(ctx);
|
||||
setResponsesRequestShapeState(sessionId, {
|
||||
updatedAt: Date.now(),
|
||||
reasoning: extractResponsesReasoningConfig(event.payload),
|
||||
text: extractResponsesTextConfig(event.payload),
|
||||
});
|
||||
const remoteState = getMatchingRemoteState(sessionId, model);
|
||||
|
||||
if (isOpenAICodexResponsesModel(model)) {
|
||||
if (!remoteState) return undefined;
|
||||
const payload = applyRemoteHistoryPayloadPatch({
|
||||
payload: event.payload,
|
||||
explicitHistory: normalizeResponseItemsForPrompt(
|
||||
remoteState.explicitHistory,
|
||||
model,
|
||||
) as unknown[],
|
||||
});
|
||||
maybeNotifyRequestFeatures({
|
||||
notifiedModels,
|
||||
hasUI: ctx.hasUI,
|
||||
notify: cfg.notify,
|
||||
ui: ctx.ui,
|
||||
model,
|
||||
features: ["remote_compaction_history"],
|
||||
});
|
||||
return payload;
|
||||
}
|
||||
|
||||
if (!remoteState) return undefined;
|
||||
|
||||
const payload = applyRemoteHistoryPayloadPatch({
|
||||
payload: event.payload,
|
||||
explicitHistory: normalizeResponseItemsForPrompt(
|
||||
remoteState.explicitHistory,
|
||||
model,
|
||||
) as unknown[],
|
||||
});
|
||||
maybeNotifyRequestFeatures({
|
||||
notifiedModels,
|
||||
hasUI: ctx.hasUI,
|
||||
notify: cfg.notify,
|
||||
ui: ctx.ui,
|
||||
model,
|
||||
features: ["remote_compaction_history", "native_http"],
|
||||
});
|
||||
|
||||
return payload;
|
||||
});
|
||||
}
|
||||
117
pi/extensions/openai-server-compaction/openai.ts
Normal file
117
pi/extensions/openai-server-compaction/openai.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* OpenAI API and OpenAI Codex model/payload helpers.
|
||||
*
|
||||
* Keeps provider-specific detection, request patching, endpoint classification,
|
||||
* and model-key logic out of the higher-level extension wiring.
|
||||
*/
|
||||
import type { JsonRecord } from "./config.ts";
|
||||
import type { ResponsesReasoningConfig, ResponsesTextConfig } from "./remote-compaction.ts";
|
||||
import { isRecord } from "./config.ts";
|
||||
|
||||
export type ModelLike = {
|
||||
api?: unknown;
|
||||
provider?: unknown;
|
||||
id?: unknown;
|
||||
baseUrl?: unknown;
|
||||
reasoning?: unknown;
|
||||
input?: readonly unknown[];
|
||||
};
|
||||
|
||||
export function hostnameFromBaseUrl(baseUrl: unknown): string | undefined {
|
||||
if (typeof baseUrl !== "string" || !baseUrl.trim()) return undefined;
|
||||
try {
|
||||
return new URL(baseUrl).hostname.toLowerCase();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function isOpenAIResponsesModel(model: unknown): model is ModelLike {
|
||||
return (
|
||||
isRecord(model) &&
|
||||
(
|
||||
model.api === "openai-responses" ||
|
||||
model.api === "openai-codex-responses"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isDirectOpenAIResponsesModel(model: ModelLike): boolean {
|
||||
if (model.api !== "openai-responses") return false;
|
||||
if (model.provider !== "openai") return false;
|
||||
const host = hostnameFromBaseUrl(model.baseUrl);
|
||||
return host === undefined || host === "api.openai.com";
|
||||
}
|
||||
|
||||
export function isOpenAICodexResponsesModel(model: ModelLike): boolean {
|
||||
if (model.api !== "openai-codex-responses") return false;
|
||||
const provider = typeof model.provider === "string" ? model.provider : "";
|
||||
if (provider === "openai-codex") return true;
|
||||
const host = hostnameFromBaseUrl(model.baseUrl);
|
||||
return host === "chatgpt.com";
|
||||
}
|
||||
|
||||
function isGptModel(model: ModelLike): boolean {
|
||||
return typeof model.id === "string" && model.id.startsWith("gpt-");
|
||||
}
|
||||
|
||||
export function supportsRemoteCompactionModel(model: unknown): model is ModelLike {
|
||||
if (!isOpenAIResponsesModel(model) || !isGptModel(model)) return false;
|
||||
return isDirectOpenAIResponsesModel(model) || isOpenAICodexResponsesModel(model);
|
||||
}
|
||||
|
||||
export function looksLikeResponsesPayload(payload: JsonRecord): boolean {
|
||||
return "input" in payload || "model" in payload || "messages" in payload;
|
||||
}
|
||||
|
||||
export function modelKey(model: ModelLike): string {
|
||||
return `${String(model.provider)}:${String(model.api)}:${String(model.id)}`;
|
||||
}
|
||||
|
||||
export function thinkingLevelToResponsesReasoning(
|
||||
thinkingLevel: unknown,
|
||||
): ResponsesReasoningConfig | undefined {
|
||||
if (thinkingLevel === "minimal") return { effort: "minimal", summary: "auto" };
|
||||
if (thinkingLevel === "low") return { effort: "low", summary: "auto" };
|
||||
if (thinkingLevel === "medium") return { effort: "medium", summary: "auto" };
|
||||
if (thinkingLevel === "high") return { effort: "high", summary: "auto" };
|
||||
if (thinkingLevel === "xhigh") return { effort: "xhigh", summary: "auto" };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function applyRemoteHistoryPayloadPatch(params: {
|
||||
payload: JsonRecord;
|
||||
explicitHistory: unknown[];
|
||||
}): JsonRecord {
|
||||
const nextPayload: JsonRecord = {
|
||||
...params.payload,
|
||||
input: params.explicitHistory,
|
||||
};
|
||||
delete nextPayload.messages;
|
||||
delete nextPayload.previous_response_id;
|
||||
return nextPayload;
|
||||
}
|
||||
|
||||
export function extractResponsesReasoningConfig(payload: unknown): ResponsesReasoningConfig | undefined {
|
||||
if (!isRecord(payload) || !isRecord(payload.reasoning)) return undefined;
|
||||
const effort = payload.reasoning.effort;
|
||||
const summary = payload.reasoning.summary;
|
||||
const normalized: ResponsesReasoningConfig = {
|
||||
...(typeof effort === "string" ? { effort: effort as ResponsesReasoningConfig["effort"] } : {}),
|
||||
...(
|
||||
summary === null || typeof summary === "string"
|
||||
? { summary: summary as ResponsesReasoningConfig["summary"] }
|
||||
: {}
|
||||
),
|
||||
};
|
||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function extractResponsesTextConfig(payload: unknown): ResponsesTextConfig | undefined {
|
||||
return isRecord(payload) && isRecord(payload.text) ? payload.text : undefined;
|
||||
}
|
||||
|
||||
export function messageMatchesModel(message: unknown, model: ModelLike): boolean {
|
||||
if (!isRecord(message)) return false;
|
||||
return message.provider === model.provider && message.model === model.id;
|
||||
}
|
||||
1070
pi/extensions/openai-server-compaction/remote-compaction.ts
Normal file
1070
pi/extensions/openai-server-compaction/remote-compaction.ts
Normal file
File diff suppressed because it is too large
Load diff
62
pi/extensions/openai-server-compaction/state.ts
Normal file
62
pi/extensions/openai-server-compaction/state.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* In-memory per-session runtime state.
|
||||
*
|
||||
* This data is intentionally ephemeral. Persisted remote compaction artifacts
|
||||
* live in Pi session entries; this module only caches the currently active
|
||||
* continuation and reconstructed replay state for the running process.
|
||||
*/
|
||||
import type {
|
||||
RemoteCompactionSessionState,
|
||||
ResponsesReasoningConfig,
|
||||
ResponsesTextConfig,
|
||||
} from "./remote-compaction.ts";
|
||||
|
||||
export type ResponsesRequestShapeState = {
|
||||
updatedAt: number;
|
||||
reasoning?: ResponsesReasoningConfig;
|
||||
text?: ResponsesTextConfig;
|
||||
};
|
||||
|
||||
const remoteCompactionBySessionId = new Map<string, RemoteCompactionSessionState>();
|
||||
const requestShapeBySessionId = new Map<string, ResponsesRequestShapeState>();
|
||||
|
||||
export function getRemoteCompactionState(
|
||||
sessionId: string,
|
||||
): RemoteCompactionSessionState | undefined {
|
||||
return remoteCompactionBySessionId.get(sessionId);
|
||||
}
|
||||
|
||||
export function setRemoteCompactionState(
|
||||
sessionId: string,
|
||||
state: RemoteCompactionSessionState,
|
||||
): void {
|
||||
remoteCompactionBySessionId.set(sessionId, state);
|
||||
}
|
||||
|
||||
export function clearRemoteCompactionState(sessionId: string | undefined): void {
|
||||
if (!sessionId) return;
|
||||
remoteCompactionBySessionId.delete(sessionId);
|
||||
}
|
||||
|
||||
export function getResponsesRequestShapeState(
|
||||
sessionId: string,
|
||||
): ResponsesRequestShapeState | undefined {
|
||||
return requestShapeBySessionId.get(sessionId);
|
||||
}
|
||||
|
||||
export function setResponsesRequestShapeState(
|
||||
sessionId: string,
|
||||
state: ResponsesRequestShapeState,
|
||||
): void {
|
||||
requestShapeBySessionId.set(sessionId, state);
|
||||
}
|
||||
|
||||
export function clearResponsesRequestShapeState(sessionId: string | undefined): void {
|
||||
if (!sessionId) return;
|
||||
requestShapeBySessionId.delete(sessionId);
|
||||
}
|
||||
|
||||
export function clearAllRuntimeState(): void {
|
||||
remoteCompactionBySessionId.clear();
|
||||
requestShapeBySessionId.clear();
|
||||
}
|
||||
|
|
@ -22,6 +22,19 @@
|
|||
"cacheRead": 0.08,
|
||||
"cacheWrite": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "kimi-k3",
|
||||
"name": "CrofAI Kimi K3",
|
||||
"reasoning": true,
|
||||
"contextWindow": 1000000,
|
||||
"maxTokens": 262144,
|
||||
"cost": {
|
||||
"input": 2,
|
||||
"output": 8,
|
||||
"cacheRead": 0.25,
|
||||
"cacheWrite": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
7
pi/skills/bro/SKILL.md
Normal file
7
pi/skills/bro/SKILL.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: bro
|
||||
description: Restate the last message in plain human language, with no jargon.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
Restate your last message. Stop using jargon and speak coherently. State it more simply and concisely, like one human talking to another.
|
||||
47
pi/skills/domain-modeling/ADR-FORMAT.md
Normal file
47
pi/skills/domain-modeling/ADR-FORMAT.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# ADR Format
|
||||
|
||||
ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc.
|
||||
|
||||
Create the `docs/adr/` directory lazily — only when the first ADR is needed.
|
||||
|
||||
## Template
|
||||
|
||||
```md
|
||||
# {Short title of the decision}
|
||||
|
||||
{1-3 sentences: what's the context, what did we decide, and why.}
|
||||
```
|
||||
|
||||
That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections.
|
||||
|
||||
## Optional sections
|
||||
|
||||
Only include these when they add genuine value. Most ADRs won't need them.
|
||||
|
||||
- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited
|
||||
- **Considered Options** — only when the rejected alternatives are worth remembering
|
||||
- **Consequences** — only when non-obvious downstream effects need to be called out
|
||||
|
||||
## Numbering
|
||||
|
||||
Scan `docs/adr/` for the highest existing number and increment by one.
|
||||
|
||||
## When to offer an ADR
|
||||
|
||||
All three of these must be true:
|
||||
|
||||
1. **Hard to reverse** — the cost of changing your mind later is meaningful
|
||||
2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?"
|
||||
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
|
||||
|
||||
If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing."
|
||||
|
||||
### What qualifies
|
||||
|
||||
- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres."
|
||||
- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP."
|
||||
- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out.
|
||||
- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s.
|
||||
- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate.
|
||||
- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract."
|
||||
- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months.
|
||||
60
pi/skills/domain-modeling/CONTEXT-FORMAT.md
Normal file
60
pi/skills/domain-modeling/CONTEXT-FORMAT.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# CONTEXT.md Format
|
||||
|
||||
## Structure
|
||||
|
||||
```md
|
||||
# {Context Name}
|
||||
|
||||
{One or two sentence description of what this context is and why it exists.}
|
||||
|
||||
## Language
|
||||
|
||||
**Order**:
|
||||
{A one or two sentence description of the term}
|
||||
_Avoid_: Purchase, transaction
|
||||
|
||||
**Invoice**:
|
||||
A request for payment sent to a customer after delivery.
|
||||
_Avoid_: Bill, payment request
|
||||
|
||||
**Customer**:
|
||||
A person or organization that places orders.
|
||||
_Avoid_: Client, buyer, account
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others under `_Avoid_`.
|
||||
- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does.
|
||||
- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs.
|
||||
- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine.
|
||||
|
||||
## Single vs multi-context repos
|
||||
|
||||
**Single context (most repos):** One `CONTEXT.md` at the repo root.
|
||||
|
||||
**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other:
|
||||
|
||||
```md
|
||||
# Context Map
|
||||
|
||||
## Contexts
|
||||
|
||||
- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders
|
||||
- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments
|
||||
- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping
|
||||
|
||||
## Relationships
|
||||
|
||||
- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking
|
||||
- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices
|
||||
- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money`
|
||||
```
|
||||
|
||||
The skill infers which structure applies:
|
||||
|
||||
- If `CONTEXT-MAP.md` exists, read it to find contexts
|
||||
- If only a root `CONTEXT.md` exists, single context
|
||||
- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved
|
||||
|
||||
When multiple contexts exist, infer which one the current topic relates to. If unclear, ask.
|
||||
74
pi/skills/domain-modeling/SKILL.md
Normal file
74
pi/skills/domain-modeling/SKILL.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
---
|
||||
name: domain-modeling
|
||||
description: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model.
|
||||
---
|
||||
|
||||
# Domain Modeling
|
||||
|
||||
Actively build and sharpen the project's domain model as you design. This is the *active* discipline — challenging terms, inventing edge-case scenarios, and writing the glossary and decisions down the moment they crystallise. (Merely *reading* `CONTEXT.md` for vocabulary is not this skill — that's a one-line habit any skill can do. This skill is for when you're changing the model, not just consuming it.)
|
||||
|
||||
## File structure
|
||||
|
||||
Most repos have a single context:
|
||||
|
||||
```
|
||||
/
|
||||
├── CONTEXT.md
|
||||
├── docs/
|
||||
│ └── adr/
|
||||
│ ├── 0001-event-sourced-orders.md
|
||||
│ └── 0002-postgres-for-write-model.md
|
||||
└── src/
|
||||
```
|
||||
|
||||
If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:
|
||||
|
||||
```
|
||||
/
|
||||
├── CONTEXT-MAP.md
|
||||
├── docs/
|
||||
│ └── adr/ ← system-wide decisions
|
||||
├── src/
|
||||
│ ├── ordering/
|
||||
│ │ ├── CONTEXT.md
|
||||
│ │ └── docs/adr/ ← context-specific decisions
|
||||
│ └── billing/
|
||||
│ ├── CONTEXT.md
|
||||
│ └── docs/adr/
|
||||
```
|
||||
|
||||
Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.
|
||||
|
||||
## During the session
|
||||
|
||||
### Challenge against the glossary
|
||||
|
||||
When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?"
|
||||
|
||||
### Sharpen fuzzy language
|
||||
|
||||
When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things."
|
||||
|
||||
### Discuss concrete scenarios
|
||||
|
||||
When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.
|
||||
|
||||
### Cross-reference with code
|
||||
|
||||
When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?"
|
||||
|
||||
### Update CONTEXT.md inline
|
||||
|
||||
When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).
|
||||
|
||||
`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.
|
||||
|
||||
### Offer ADRs sparingly
|
||||
|
||||
Only offer to create an ADR when all three are true:
|
||||
|
||||
1. **Hard to reverse** — the cost of changing your mind later is meaningful
|
||||
2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
|
||||
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
|
||||
|
||||
If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
---
|
||||
name: grill-me
|
||||
description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
|
||||
description: A relentless interview to sharpen a plan or design.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
|
||||
|
||||
Ask the questions one at a time.
|
||||
|
||||
If a question can be answered by exploring the codebase, explore the codebase instead.
|
||||
Run a `/grilling` session.
|
||||
|
|
|
|||
7
pi/skills/grill-with-docs/SKILL.md
Normal file
7
pi/skills/grill-with-docs/SKILL.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: grill-with-docs
|
||||
description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
Run a `/grilling` session, using the `/domain-modeling` skill.
|
||||
12
pi/skills/grilling/SKILL.md
Normal file
12
pi/skills/grilling/SKILL.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
name: grilling
|
||||
description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
|
||||
---
|
||||
|
||||
Interview me relentlessly about every aspect of this until we reach a shared understanding. Walk down each branch of the decision tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
|
||||
|
||||
Ask the questions one at a time, waiting for feedback on each question before continuing. Asking multiple questions at once is bewildering.
|
||||
|
||||
If a *fact* can be found by exploring the environment (filesystem, tools, etc.), look it up rather than asking me. The *decisions*, though, are mine — put each one to me and wait for my answer.
|
||||
|
||||
Do not act on it until I confirm we have reached a shared understanding.
|
||||
Loading…
Add table
Add a link
Reference in a new issue