import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { StringEnum } from "@earendil-works/pi-ai"; import { type ExtensionAPI, defineTool } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import { Type } from "typebox"; const NoteLevel = StringEnum(["info", "milestone", "blocked"]); const MAX_NOTE_CHARS = 200; export interface NoteEntry { text: string; level: "info" | "milestone" | "blocked"; timestamp: number; } export interface NoteDetails { level: string; chars: number; } export interface NoteDeps { /** * R-ORCH-16 / R-TOOL-21: `blocked` suppresses sleep. Reported to the Scheduler so * it neither arms a wake (§11.2) nor lets `agi_sleep` succeed for the rest of the * turn — a blocked orchestrator that sleeps wakes still blocked, forever. */ onLevel?: (level: NoteEntry["level"]) => void; } export interface NoteEmitter { emit(text: string, level?: NoteEntry["level"]): NoteEntry; hasEmitted(): boolean; } export function registerNoteTool(pi: ExtensionAPI, deps: NoteDeps = {}): NoteEmitter { let emittedThisTurn = false; const emitter: NoteEmitter = { hasEmitted: () => emittedThisTurn, emit(rawText, level = "info") { if (emittedThisTurn) throw new Error("agi_note: only one progress note is allowed per agent run."); const text = rawText.trim(); if (text.length === 0) throw new Error("agi_note: text is empty."); if (text.length > MAX_NOTE_CHARS) throw new Error(`agi_note: text exceeds ${MAX_NOTE_CHARS} characters.`); const entry = { text, level, timestamp: Date.now() }; emittedThisTurn = true; deps.onLevel?.(level); pi.appendEntry("agi-note", entry); return entry; }, }; pi.registerEntryRenderer("agi-note", (entry, _options, theme) => { const data = entry.data; if (data === undefined) return undefined; const colour = data.level === "blocked" ? "error" : data.level === "milestone" ? "success" : "accent"; const label = data.level === "info" ? "AGI" : `AGI ${data.level}`; return new Text(`${theme.fg(colour, `[${label}]`)} ${data.text}`, 0, 0); }); // One user-facing progress note per whole agent run/wake, not per individual // LLM generation. A tool call starts another `turn_start`, so resetting there // allowed a second note later in the same wake episode. pi.on("agent_start", async () => { emittedThisTurn = false; }); pi.registerTool( defineTool({ name: "agi_note", label: "AGI Note", description: "Emit one short progress note to the user for a meaningful state change: delegation started, " + "milestone reached, or a decision is blocked. Keep routine work in the normal assistant and tool flow. " + "Notes are durable session entries stored outside model context.", promptSnippet: "agi_note: emit a short user-facing milestone or blocker", parameters: Type.Object({ text: Type.String({ minLength: 1, maxLength: MAX_NOTE_CHARS, description: "Concise progress note, at most 200 characters.", }), level: Type.Optional(NoteLevel), }), execute: async (_id, params): Promise> => { const level: NoteEntry["level"] = params.level === "milestone" || params.level === "blocked" ? params.level : "info"; const entry = emitter.emit(params.text, level); return { content: [{ type: "text", text: "Progress note shown to the user." }], details: { level, chars: entry.text.length }, }; }, }), ); return emitter; }