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, }; }