195 lines
6.1 KiB
JavaScript
195 lines
6.1 KiB
JavaScript
import { Type } from "typebox";
|
|
|
|
|
|
export const NEW_SESSION_CLARIFICATION_KICKOFF =
|
|
"2. Nel primo turno identifica la SOLA ambiguità con maggiore impatto sulla query e " +
|
|
"chiama subito `reviewer_select` con opzioni concrete. Niente lunga narrazione, elenco " +
|
|
"di tutte le ambiguità o ricapitolazione preliminare.\n";
|
|
|
|
|
|
export const EMPTY_SESSION_CLARIFICATION_KICKOFF =
|
|
"Se la sessione e' in fase 1 e non ha decisioni, dopo `session show` chiama " +
|
|
"`reviewer_select` subito: non produrre testo libero.\n";
|
|
|
|
|
|
function normalizedAssumptions(assumptions) {
|
|
let normalized = assumptions;
|
|
if (typeof normalized === "string") {
|
|
try {
|
|
normalized = JSON.parse(normalized);
|
|
} catch {
|
|
normalized = [normalized];
|
|
}
|
|
}
|
|
return Array.isArray(normalized) ? normalized.map(String) : [];
|
|
}
|
|
|
|
|
|
function prepareClarificationArguments(input) {
|
|
if (!input || typeof input !== "object") return input;
|
|
const args = { ...input };
|
|
if (typeof args.options === "string") {
|
|
try {
|
|
const parsed = JSON.parse(args.options);
|
|
if (Array.isArray(parsed)) args.options = parsed;
|
|
} catch {
|
|
// Preserve non-JSON input for the TypeBox/tool validation path.
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
|
|
function decisionRecordedText(decision, option) {
|
|
return (
|
|
`Decisione registrata (${decision.type}): ${option.label}. ` +
|
|
"Ora rileggi lo stato persistito con `tht session show` e invoca immediatamente " +
|
|
"il prossimo tool reviewer_ richiesto dal workflow. " +
|
|
"Non scrivere analisi o spiegazioni visibili."
|
|
);
|
|
}
|
|
|
|
|
|
function validateClarificationOptions(ctx, options, session, workflow) {
|
|
const known = workflow.decisionTypes(ctx);
|
|
if (!known) return null;
|
|
for (const option of options) {
|
|
if (option.decision && !known.has(option.decision.type)) {
|
|
return (
|
|
`Tipo di decisione '${option.decision.type}' non valido. Tipi ammessi: ` +
|
|
`${[...known].join(", ")}. Correggi e riprova.`
|
|
);
|
|
}
|
|
}
|
|
const current = workflow.phase(ctx, session).number;
|
|
const minimumPhases = workflow.decisionMinimumPhases(ctx);
|
|
for (const option of options) {
|
|
const type = option.decision?.type;
|
|
if (type && minimumPhases[type] && minimumPhases[type] > current) {
|
|
return (
|
|
`Tipo '${type}' ammesso dalla Fase ${minimumPhases[type]}, ` +
|
|
`sessione alla Fase ${current}. Chiudi prima la fase corrente.`
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
function prepareClarification(
|
|
ctx,
|
|
params,
|
|
id,
|
|
{ workflow, reviewer, toTextResult },
|
|
) {
|
|
const { session, title, options, intro, advance } = params;
|
|
const validationError = validateClarificationOptions(ctx, options, session, workflow);
|
|
if (validationError) return { result: toTextResult(validationError) };
|
|
const phase = workflow.describe(ctx, session);
|
|
const recommended = options.find((option) => option.recommended)?.id ?? null;
|
|
return {
|
|
session,
|
|
options,
|
|
advance,
|
|
widget: reviewer.buildSelect({
|
|
id,
|
|
phase,
|
|
title,
|
|
intro: intro ?? null,
|
|
recommended,
|
|
options: options
|
|
.filter((option) => !reviewer.isReserved(option.label))
|
|
.map((option) => ({ id: option.id, label: option.label })),
|
|
}),
|
|
};
|
|
}
|
|
|
|
|
|
function resolveClarification(prepared, response) {
|
|
if (response?.control === "freetext") {
|
|
return { text: `Altro (reviewer): ${response.text}` };
|
|
}
|
|
if (response?.control === "back") {
|
|
return { text: "Il reviewer vuole tornare indietro." };
|
|
}
|
|
if (response?.control === "exit") {
|
|
return { text: "Il reviewer vuole uscire." };
|
|
}
|
|
const choice = Array.isArray(response?.choices) ? response.choices[0] : response?.choice;
|
|
const option = (prepared.options || []).find((candidate) => candidate.id === choice) || null;
|
|
if (option?.decision) {
|
|
return {
|
|
decision: option.decision,
|
|
text: decisionRecordedText(option.decision, option),
|
|
};
|
|
}
|
|
return { text: `Scelta del reviewer: ${option ? option.label : choice}` };
|
|
}
|
|
|
|
|
|
function installDisambiguationGate(
|
|
pi,
|
|
{ workflow, session, ledger, toTextResult },
|
|
) {
|
|
// Compatibility: tool schema, labels and messages move verbatim from the
|
|
// composition root; this extraction changes ownership, not Pi behavior.
|
|
pi.registerTool({
|
|
name: "rewrite_question",
|
|
label: "Riscrittura domanda e chiusura F3 (deterministica)",
|
|
description:
|
|
"In Fase 3 scrive deterministicamente question.md, registra question_rewritten e " +
|
|
"chiude la fase senza chiedere conferma al reviewer. assumptions puo' essere array " +
|
|
"o stringa JSON.",
|
|
parameters: Type.Object({
|
|
session: Type.String(),
|
|
question: Type.String(),
|
|
assumptions: Type.Optional(Type.Any()),
|
|
}),
|
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
workflow.activate();
|
|
try {
|
|
const { session: sessionId, question } = params;
|
|
const current = workflow.phase(ctx, sessionId);
|
|
if (current.number !== 3) {
|
|
return toTextResult("La riscrittura automatica e' disponibile solo in Fase 3.");
|
|
}
|
|
// `tht session set-question` takes the session id as a positional argument
|
|
// (unlike phase/cte/decision, which use --session).
|
|
const args = ["set-question", sessionId, "--question", question];
|
|
for (const assumption of normalizedAssumptions(params.assumptions)) {
|
|
args.push("--assumption", assumption);
|
|
}
|
|
const mutationError = session.mutate(ctx, args, "");
|
|
if (mutationError) return mutationError;
|
|
const decisionError = ledger.record(ctx, sessionId, {
|
|
type: "question_rewritten",
|
|
subject: "domanda",
|
|
detail: question,
|
|
}, "");
|
|
if (decisionError) return decisionError;
|
|
const advanced = workflow.advance(ctx, sessionId, current.number);
|
|
if (advanced.err) return advanced.err;
|
|
return toTextResult(
|
|
`Domanda riscritta e Fase 3 completata (sessione ${sessionId}).`,
|
|
);
|
|
} catch (fatal) {
|
|
const message = (fatal.stderr || fatal.message || String(fatal)).toString().trim();
|
|
return toTextResult(
|
|
`[rewrite_question ERRORE INTERNO] ${message}. Riprova o usa un approccio diverso.`,
|
|
);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
|
|
export function createDisambiguationGate(dependencies) {
|
|
return {
|
|
install: (pi) => installDisambiguationGate(pi, dependencies),
|
|
prepareClarificationArguments,
|
|
prepareClarification: (ctx, params, id) =>
|
|
prepareClarification(ctx, params, id, dependencies),
|
|
resolveClarification,
|
|
};
|
|
}
|