feat(config): add askUser capability and shared ask contract

Introduce the shared contract for the upcoming ask_user tool: question and
pending-ask types in the API contract, the pendingAsk field on
SessionStatus, ask.opened/ask.closed session UI events, the askUser global
config key with a PI_WEB_ASK_USER env override, and the sessions.askUser
capability requiring both the web and session daemon runtimes.

askUser defaults to true: the questions land in the session the user is
already watching and nothing happens until they act, unlike the beta-off
subsessions flag.
This commit is contained in:
Federico Jaramillo Martinez
2026-07-26 21:26:23 +02:00
parent 2a05d67436
commit 6fa57b524b
23 changed files with 181 additions and 15 deletions
+25 -1
View File
@@ -15,10 +15,11 @@ export interface LoadedPiWebConfig {
config: PiWebConfig;
}
export interface EffectivePiWebConfig extends Omit<PiWebConfig, "uploads" | "spawnSessions" | "subsessions" | "agent"> {
export interface EffectivePiWebConfig extends Omit<PiWebConfig, "uploads" | "spawnSessions" | "subsessions" | "askUser" | "agent"> {
uploads: NonNullable<PiWebConfig["uploads"]>;
spawnSessions: boolean;
subsessions: boolean;
askUser: boolean;
agent: Required<NonNullable<PiWebConfig["agent"]>>;
}
@@ -156,6 +157,8 @@ export function resolveEffectivePiWebConfig(loaded: LoadedPiWebConfig, options:
spawnSessions: spawnSessionsEnabled(env, loaded.config),
// Beta capability, resolved off by default.
subsessions: subsessionsEnabled(env, loaded.config),
// Always resolved (on by default); the user is present for every ask.
askUser: askUserEnabled(env, loaded.config),
agent: { command: agent.command, dir: agent.dir },
},
};
@@ -178,6 +181,7 @@ export function savePiWebConfig(config: PiWebConfig, options: LoadOptions = {}):
delete existing["maxUploadBytes"];
delete existing["spawnSessions"];
delete existing["subsessions"];
delete existing["askUser"];
delete existing["agent"];
const merged = { ...existing, ...piWebConfigRecord(normalized) };
mkdirSync(dirname(path), { recursive: true });
@@ -204,6 +208,7 @@ function piWebConfigRecord(config: PiWebConfig): Record<string, unknown> {
...(config.maxUploadBytes !== undefined ? { maxUploadBytes: config.maxUploadBytes } : {}),
...(config.spawnSessions !== undefined ? { spawnSessions: config.spawnSessions } : {}),
...(config.subsessions !== undefined ? { subsessions: config.subsessions } : {}),
...(config.askUser !== undefined ? { askUser: config.askUser } : {}),
...(config.agent !== undefined ? { agent: config.agent } : {}),
};
}
@@ -220,6 +225,7 @@ function parsePiWebConfig(value: Record<string, unknown>, path: string): PiWebCo
...(value["maxUploadBytes"] !== undefined ? { maxUploadBytes: parseMaxUploadBytes(value["maxUploadBytes"], "maxUploadBytes", path) } : {}),
...(value["spawnSessions"] !== undefined ? { spawnSessions: parseSpawnSessions(value["spawnSessions"], path) } : {}),
...(value["subsessions"] !== undefined ? { subsessions: parseSubsessions(value["subsessions"], path) } : {}),
...(value["askUser"] !== undefined ? { askUser: parseAskUser(value["askUser"], path) } : {}),
...(value["agent"] !== undefined ? { agent: parseAgentConfig(value["agent"], path) } : {}),
};
}
@@ -266,6 +272,24 @@ export function subsessionsEnabled(env: NodeJS.ProcessEnv = process.env, config:
return config.subsessions ?? false;
}
function parseAskUser(value: unknown, path: string): boolean {
if (typeof value !== "boolean") throw new Error(`PI WEB config askUser must be a boolean: ${path}`);
return value;
}
/**
* Whether LLMs may post a question set to the browser via the ask_user tool. On
* by default: the questions land in the session the user is already watching and
* nothing happens without them acting. Set the env var `PI_WEB_ASK_USER` or the
* `askUser` config key to `false` to remove the tool. The env var takes
* precedence over the config file.
*/
export function askUserEnabled(env: NodeJS.ProcessEnv = process.env, config: PiWebConfig = {}): boolean {
const fromEnv = env["PI_WEB_ASK_USER"];
if (fromEnv !== undefined && fromEnv !== "") return fromEnv === "1" || fromEnv.toLowerCase() === "true";
return config.askUser ?? true;
}
const OFFLINE_ENV_KEYS = ["PI_WEB_OFFLINE", "PI_OFFLINE"] as const;
/**