mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
AutoReview: - 审查器回到 Codex Guardian 风格的对话式审查,默认跟随会话当前主模型 (每次审查实时读取 ctx.model);provider/model 必须同时设置才固定审查 模型,只设其一视为配置错误并转人工。 - 结论只通过 approve / reject 两个工具给出,参数仅 reason;不强制 toolChoice,正文文本(即使是合法 JSON)一律忽略。 - approve 自动放行;reject 转人工并在弹窗显示理由(标注模型生成)。 超时、Provider 错误、截断、未调用工具、多次调用、参数无效等异常同样 转人工,并附固定的系统说明;用户按 Esc 中止时直接拒绝。 - 模型调用改为 modelRegistry.streamSimple(...).result():Pi 1.3.0 面向 扩展的 ModelRegistry 没有 completeSimple,原写法在真实环境中每次都会 转人工。 - 对话记录保留可信度标注;工具结果在有更短的 pi-compaction 摘要时替换 为摘要;token 估算改为非 ASCII 字符按 1 token 计;预算可配置。 - 同一条用户消息内完全相同的请求只缓存 approve 结论。 - 默认 reasoning low、总超时 30 秒、最多 2 次尝试、maxOutputTokens 4000。 删除: - 未发布过的 TypeSafe Jev 分类器实现、DenialCircuitBreaker。 - 整个 annotator 机制(model-explanation、注册表、registerAnnotator、 provider-timeout);annotators 配置键保留为已弃用并忽略,避免旧配置 被拒绝。 测试: - 新增 reviewer / transcript / 弹窗说明 / 默认配置测试;清理重复、恒真 或只测实现细节的旧测试。 - 使用 deepseek-v4.1-flash 实测 5 个场景各 5 次,25 次结论全部符合预期。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
184 lines
5.7 KiB
TypeScript
184 lines
5.7 KiB
TypeScript
import type { DecisionSource } from "#src/authority/decision-source";
|
|
import type { PromptDecisionNote } from "#src/presentation/prompt-payload";
|
|
|
|
export type PermissionDecisionState =
|
|
| "approved"
|
|
| "approved_for_session"
|
|
| "approved_for_serving_session"
|
|
| "denied"
|
|
| "denied_with_reason";
|
|
|
|
export type PermissionPromptDecision = {
|
|
approved: boolean;
|
|
state: PermissionDecisionState;
|
|
denialReason?: string;
|
|
/**
|
|
* True when the decision was made automatically by yolo mode rather than
|
|
* by an interactive user prompt. Used by handlers to emit "auto_approved"
|
|
* rather than "user_approved" in the permissions:decision broadcast.
|
|
*/
|
|
autoApproved?: true;
|
|
/**
|
|
* True when no human ever ruled on this ask: either no live authority was
|
|
* reachable at all (`DenyingAuthorizer`, a no-UI non-subagent session) or the
|
|
* forwarding path gave up before reaching one (`ParentAuthorizer` — target
|
|
* unresolvable, request undeliverable, target not serving, or no answer
|
|
* within the timeout). Consumed by deriveResolution (the decision-event
|
|
* resolution), the gate (block reason), and PermissionPrompter (review-entry
|
|
* resolution) to emit "confirmation_unavailable" rather than a plain user
|
|
* denial — a user who was never asked denied nothing (#719).
|
|
*/
|
|
confirmationUnavailable?: true;
|
|
/**
|
|
* What decided this request, stamped by the site that decided it.
|
|
*
|
|
* Required: every decision names its decider, and the type is what
|
|
* guarantees it rather than a convention each producer has to remember — the
|
|
* same discipline `PromptPermissionDetails.payload` carries (#726).
|
|
*/
|
|
decidedBy: DecisionSource;
|
|
};
|
|
|
|
/**
|
|
* A decision before its decider is known.
|
|
*
|
|
* The inner producers — the dialog's decision model, the `select`/`input`
|
|
* fallback, the verdict mapper — state the outcome; which decider to attribute
|
|
* it to is settled one layer up, at the site that chose the producer. The same
|
|
* shape `GateBypass.decision` uses for the request id: a producer emits only
|
|
* what it knows.
|
|
*/
|
|
export type UnattributedDecision = Omit<PermissionPromptDecision, "decidedBy">;
|
|
|
|
export interface PermissionDecisionUi {
|
|
select(title: string, options: string[]): Promise<string | undefined>;
|
|
input(title: string, placeholder?: string): Promise<string | undefined>;
|
|
}
|
|
|
|
const APPROVE_OPTION = "Yes";
|
|
const APPROVE_FOR_SESSION_OPTION = "Yes, for this session";
|
|
const DENY_OPTION = "No";
|
|
const DENY_WITH_REASON_OPTION = "No, provide reason";
|
|
|
|
export function normalizePermissionDenialReason(
|
|
value: unknown,
|
|
): string | undefined {
|
|
if (typeof value !== "string") {
|
|
return undefined;
|
|
}
|
|
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
}
|
|
|
|
export function createDeniedPermissionDecision(
|
|
denialReason?: string,
|
|
): UnattributedDecision {
|
|
const normalizedReason = normalizePermissionDenialReason(denialReason);
|
|
return normalizedReason
|
|
? {
|
|
approved: false,
|
|
state: "denied_with_reason",
|
|
denialReason: normalizedReason,
|
|
}
|
|
: {
|
|
approved: false,
|
|
state: "denied",
|
|
};
|
|
}
|
|
|
|
export function isPermissionDecisionState(
|
|
value: unknown,
|
|
): value is PermissionDecisionState {
|
|
return (
|
|
value === "approved" ||
|
|
value === "approved_for_session" ||
|
|
value === "approved_for_serving_session" ||
|
|
value === "denied" ||
|
|
value === "denied_with_reason"
|
|
);
|
|
}
|
|
|
|
export interface RequestPermissionOptions {
|
|
/** Override the "for this session" option label (e.g. to show the suggested pattern). */
|
|
sessionLabel?: string;
|
|
/**
|
|
* Forwarded asks only: when set, choosing the "for this session" option opens
|
|
* a second select asking whether the grant applies to the requesting subagent
|
|
* only (the least-privilege default) or the whole serving session.
|
|
*/
|
|
sessionScope?: {
|
|
subagentLabel: string;
|
|
servingSessionLabel: string;
|
|
};
|
|
/**
|
|
* Why automatic review declined to approve, shown ahead of the ask facts.
|
|
*
|
|
* `origin` marks who wrote each note: a `model` reason is the reviewer's own
|
|
* verdict text and renders marked as model-generated; a `system` note is a
|
|
* deterministic, code-authored sentence.
|
|
*/
|
|
deferNotes?: readonly PromptDecisionNote[];
|
|
}
|
|
|
|
export async function requestPermissionDecisionFromUi(
|
|
ui: PermissionDecisionUi,
|
|
title: string,
|
|
message: string,
|
|
options?: RequestPermissionOptions,
|
|
): Promise<UnattributedDecision> {
|
|
const sessionOption = options?.sessionLabel ?? APPROVE_FOR_SESSION_OPTION;
|
|
const decisionOptions = [
|
|
APPROVE_OPTION,
|
|
sessionOption,
|
|
DENY_OPTION,
|
|
DENY_WITH_REASON_OPTION,
|
|
] as const;
|
|
|
|
const selected = await ui.select(`${title}\n${message}`, [
|
|
...decisionOptions,
|
|
]);
|
|
|
|
if (selected === APPROVE_OPTION) {
|
|
return {
|
|
approved: true,
|
|
state: "approved",
|
|
};
|
|
}
|
|
|
|
if (selected === sessionOption) {
|
|
if (options?.sessionScope) {
|
|
const scope = await ui.select(`${title}\nApply this session grant to:`, [
|
|
options.sessionScope.subagentLabel,
|
|
options.sessionScope.servingSessionLabel,
|
|
]);
|
|
return {
|
|
approved: true,
|
|
// A cancelled scope select (undefined) falls back to the
|
|
// least-privilege subagent scope.
|
|
state:
|
|
scope === options.sessionScope.servingSessionLabel
|
|
? "approved_for_serving_session"
|
|
: "approved_for_session",
|
|
};
|
|
}
|
|
return {
|
|
approved: true,
|
|
state: "approved_for_session",
|
|
};
|
|
}
|
|
|
|
if (selected === DENY_WITH_REASON_OPTION) {
|
|
const denialReason = normalizePermissionDenialReason(
|
|
await ui.input(
|
|
`${title}\nShare why this request was denied (optional).`,
|
|
"Reason shown back to the agent",
|
|
),
|
|
);
|
|
|
|
return createDeniedPermissionDecision(denialReason);
|
|
}
|
|
|
|
return createDeniedPermissionDecision();
|
|
}
|