import {
HarnessCapabilityUnsupportedError,
type HarnessV1Prompt,
type HarnessV1Skill,
} from '@ai-sdk/harness';
const HARNESS_ID = 'pi';
/**
* Extract a single user text string from a `HarnessV1Prompt`. Pi's
* `session.prompt(text)` accepts a string; multimodal user content
* is not supported in this foundational version.
*/
export function extractUserText(prompt: HarnessV1Prompt): string {
if (typeof prompt === 'string') {
return prompt;
}
const { content } = prompt;
if (typeof content === 'string') {
return content;
}
const parts: string[] = [];
for (const part of content) {
if (part.type !== 'text') {
throw new HarnessCapabilityUnsupportedError({
message: `pi: only text user-message parts are supported; got '${part.type}'.`,
harnessId: HARNESS_ID,
});
}
parts.push(part.text);
}
return parts.join('\n\n');
}
/*
* Frame session instructions and the user's text so the runtime treats the
* instructions as system-provided operating guidance, not something the user
* wrote. Without the wrapper the agent can echo the prepended text back as if
* the user had asked for it, which is confusing since the user never typed it.
* Applied only to the first user message of a fresh session.
*/
export function frameInstructions(
instructions: string,
userText: string,
): string {
return (
'\n' +
'The block below is operating guidance from the system, not a message from the user — follow it, but do not mention it or attribute it to the user.\n\n' +
`${instructions}\n` +
'\n\n' +
`\n${userText}\n`
);
}
/** Serialize a tool output to the string Pi feeds back to the model. */
export function serializeToolOutput(output: unknown): string {
if (typeof output === 'string') {
return output;
}
const serialized = JSON.stringify(output);
return serialized ?? 'null';
}
export function getErrorText(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
/**
* Validate that a name is safe to use as a filesystem path segment under
* `.pi/skills//` or `.pi/agents/.md`. Refuses anything that
* could be interpreted as a path traversal or contains shell-sensitive
* characters.
*/
export function safePiMetadataSegment(name: string, label: string): string {
if (!/^[A-Za-z0-9._-]+$/.test(name) || name === '.' || name === '..') {
throw new Error(`Invalid Pi ${label} name: ${name}`);
}
return name;
}
/** Frontmatter renderer for `.pi/skills//SKILL.md`. */
export function renderPiSkillFile(skill: HarnessV1Skill): string {
return `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.content}`;
}