feat: compact vs. verbose chat settings
This commit is contained in:
@@ -218,6 +218,7 @@ execution:
|
|||||||
reviewTimeoutMs: 0 # timeout for auto-review agent sessions (0 = inherit)
|
reviewTimeoutMs: 0 # timeout for auto-review agent sessions (0 = inherit)
|
||||||
loopTimeoutMs: 0 # max total loop duration in ms (0 = no limit; checked between batches)
|
loopTimeoutMs: 0 # max total loop duration in ms (0 = no limit; checked between batches)
|
||||||
worktrees: parallel # "never" | "parallel" (default) | "always" — git worktree isolation
|
worktrees: parallel # "never" | "parallel" (default) | "always" — git worktree isolation
|
||||||
|
chatStyle: compact # "compact" (default) | "verbose" — per-event tool-call stream
|
||||||
prompts:
|
prompts:
|
||||||
projectContext: "Additional context for all tasks"
|
projectContext: "Additional context for all tasks"
|
||||||
reflectionPrompt: "" # custom suffix for reflection extraction
|
reflectionPrompt: "" # custom suffix for reflection extraction
|
||||||
@@ -251,6 +252,10 @@ them.
|
|||||||
> (or parallel mode with no `models` list) the parent pi session's model is used,
|
> (or parallel mode with no `models` list) the parent pi session's model is used,
|
||||||
> unless `implModel` is set.
|
> unless `implModel` is set.
|
||||||
|
|
||||||
|
> `execution.chatStyle` controls how sub-agent tool calls appear in the chat during task execution:
|
||||||
|
> - **compact** (default): a single completion message per task with an expandable tool-call tree (collapsed shows the last 3 calls, expanded via Ctrl+O shows all).
|
||||||
|
> - **verbose**: each tool event is streamed live as its own chat line (`[01 · task-name] → bash ...` / `← (ok)`), like piolium/pygienium's per-event stream.
|
||||||
|
|
||||||
#### Auto-review and Auto-commit
|
#### Auto-review and Auto-commit
|
||||||
|
|
||||||
At loop startup the review question is asked FIRST. When `autoReview` is
|
At loop startup the review question is asked FIRST. When `autoReview` is
|
||||||
|
|||||||
194
index.ts
194
index.ts
@@ -19,7 +19,7 @@ import { loadTaskManagerPrompt } from "./src/task-manager-prompt";
|
|||||||
import { formatReflections } from "./src/reflection";
|
import { formatReflections } from "./src/reflection";
|
||||||
import { verdictGlyph, verdictSummary, formatFindings } from "./src/review";
|
import { verdictGlyph, verdictSummary, formatFindings } from "./src/review";
|
||||||
import type { ReviewResult } from "./src/types";
|
import type { ReviewResult } from "./src/types";
|
||||||
import { executeBatch, type SendChatMessage } from "./src/executor";
|
import { executeBatch, type SendChatMessage, setStreamForwarder } from "./src/executor";
|
||||||
import {
|
import {
|
||||||
cleanupStaleWorktrees,
|
cleanupStaleWorktrees,
|
||||||
finalizeCommittedWorktrees,
|
finalizeCommittedWorktrees,
|
||||||
@@ -469,9 +469,134 @@ function makeSendProgress(pi: ExtensionAPI): SendChatMessage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Pick the one useful argument from a tool-call's args (path/command/…). */
|
||||||
|
function summarizeArgs(args: unknown): string {
|
||||||
|
if (!args || typeof args !== "object") return "";
|
||||||
|
const obj = args as Record<string, unknown>;
|
||||||
|
const pickKey = ["file_path", "path", "command", "pattern", "query", "url"].find(
|
||||||
|
(k) => typeof obj[k] === "string",
|
||||||
|
);
|
||||||
|
if (pickKey) {
|
||||||
|
const value = String(obj[pickKey]);
|
||||||
|
return value.length > 120 ? `${value.slice(0, 117)}…` : value;
|
||||||
|
}
|
||||||
|
const json = JSON.stringify(obj);
|
||||||
|
return json.length > 120 ? `${json.slice(0, 117)}…` : json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Collapse a tool result down to a single short line. */
|
||||||
|
function summarizeToolResult(result: unknown): string {
|
||||||
|
if (result == null) return "";
|
||||||
|
if (typeof result === "string") return result;
|
||||||
|
if (typeof result === "number" || typeof result === "boolean")
|
||||||
|
return String(result);
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
return result
|
||||||
|
.map((item) => {
|
||||||
|
if (typeof item === "string") return item;
|
||||||
|
if (
|
||||||
|
item &&
|
||||||
|
typeof item === "object" &&
|
||||||
|
"text" in (item as Record<string, unknown>)
|
||||||
|
)
|
||||||
|
return String((item as { text?: unknown }).text ?? "");
|
||||||
|
return JSON.stringify(item);
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
if (typeof result !== "object") return "";
|
||||||
|
const obj = result as Record<string, unknown>;
|
||||||
|
if (Array.isArray(obj.content)) {
|
||||||
|
const unwrapped = summarizeToolResult(obj.content);
|
||||||
|
if (unwrapped) return unwrapped;
|
||||||
|
}
|
||||||
|
const preferKey = ["stdout", "output", "text", "content", "result"].find(
|
||||||
|
(k) => typeof obj[k] === "string" && (obj[k] as string).length > 0,
|
||||||
|
);
|
||||||
|
if (preferKey) return obj[preferKey] as string;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(obj);
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Collapse whitespace and cap a line at `max` chars with an ellipsis. */
|
||||||
|
function compactLine(text: string, max: number): string {
|
||||||
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
||||||
|
if (collapsed.length <= max) return collapsed;
|
||||||
|
return `${collapsed.slice(0, max - 1)}…`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract joined text from an assistant message's content blocks. */
|
||||||
|
function extractAssistantTextFromContent(content: unknown): string {
|
||||||
|
if (typeof content === "string") return content;
|
||||||
|
if (!Array.isArray(content)) return "";
|
||||||
|
return content
|
||||||
|
.flatMap((c) =>
|
||||||
|
c && typeof c === "object" &&
|
||||||
|
(c as { type?: string }).type === "text"
|
||||||
|
? [(c as { text?: string }).text ?? ""]
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a stream-event forwarder that posts ralpi-stream messages (one chat
|
||||||
|
* line per tool start/end and assistant turn) into the chat. Only used when
|
||||||
|
* execution.chatStyle is "verbose".
|
||||||
|
*/
|
||||||
|
function makeStreamForwarder(pi: ExtensionAPI): (phase: string, event: import("@earendil-works/pi-coding-agent").AgentSessionEvent) => void {
|
||||||
|
const send = (details: {
|
||||||
|
kind: "tool-start" | "tool-end" | "tool-error" | "assistant";
|
||||||
|
phase: string;
|
||||||
|
toolName?: string;
|
||||||
|
body?: string;
|
||||||
|
}, fallback: string) => {
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "ralpi-stream",
|
||||||
|
content: fallback,
|
||||||
|
display: true,
|
||||||
|
details,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (phase: string, event: import("@earendil-works/pi-coding-agent").AgentSessionEvent) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case "tool_execution_start": {
|
||||||
|
const body = summarizeArgs(event.args);
|
||||||
|
send({ kind: "tool-start", phase, toolName: event.toolName, body }, `[${phase}] → ${event.toolName}${body ? ` ${body}` : ""}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "tool_execution_end": {
|
||||||
|
const body = compactLine(summarizeToolResult(event.result), 200);
|
||||||
|
const kind = event.isError ? "tool-error" : "tool-end";
|
||||||
|
const marker = event.isError ? "✗" : "←";
|
||||||
|
send({ kind, phase, toolName: event.toolName, body }, `[${phase}] ${marker} ${event.toolName}${body ? ` ${body}` : ""}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "message_end": {
|
||||||
|
const message = event.message as { role?: string; content?: unknown };
|
||||||
|
if (message.role !== "assistant") return;
|
||||||
|
const text = extractAssistantTextFromContent(message.content).trim();
|
||||||
|
if (!text) return;
|
||||||
|
const head = compactLine(text, 240);
|
||||||
|
send({ kind: "assistant", phase, body: head }, `[${phase}] ${head}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Extension Entry ────────────────────────────────────────────────────────
|
// ─── Extension Entry ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function ralpiLoopExtension(pi: ExtensionAPI): void {
|
export default function ralpiLoopExtension(pi: ExtensionAPI): void {
|
||||||
|
// Wire the verbose stream forwarder — posts each tool event as its own
|
||||||
|
// chat message via the ralpi-stream renderer. Enabled per-run by
|
||||||
|
// `execution.chatStyle: verbose` in the config YAML.
|
||||||
|
setStreamForwarder(makeStreamForwarder(pi));
|
||||||
|
|
||||||
// Register custom message renderer for ralpi progress messages.
|
// Register custom message renderer for ralpi progress messages.
|
||||||
// Renders an expandable tool-call tree: collapsed shows last 3 + "N more",
|
// Renders an expandable tool-call tree: collapsed shows last 3 + "N more",
|
||||||
// expanded (Ctrl+O) shows every tool call.
|
// expanded (Ctrl+O) shows every tool call.
|
||||||
@@ -572,6 +697,73 @@ export default function ralpiLoopExtension(pi: ExtensionAPI): void {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ─── Verbose tool-event stream renderer ─────────────────────────────
|
||||||
|
//
|
||||||
|
// When execution.chatStyle is "verbose", each tool_execution_start/end and
|
||||||
|
// assistant turn is posted as its own chat message — the piolium/pygienium
|
||||||
|
// per-event stream. When "compact" (default), only the completion message
|
||||||
|
// with its expandable tool-call tree shows (the existing ralpi-progress
|
||||||
|
// renderer above).
|
||||||
|
|
||||||
|
type StreamLineKind = "tool-start" | "tool-end" | "tool-error" | "assistant";
|
||||||
|
|
||||||
|
interface StreamLineDetails {
|
||||||
|
kind: StreamLineKind;
|
||||||
|
phase: string;
|
||||||
|
toolName?: string;
|
||||||
|
body?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
pi.registerMessageRenderer<StreamLineDetails>(
|
||||||
|
"ralpi-stream",
|
||||||
|
(message, _options, theme) => {
|
||||||
|
const details = message.details;
|
||||||
|
if (!details || typeof details !== "object") {
|
||||||
|
const fallback =
|
||||||
|
typeof message.content === "string" ? message.content : "";
|
||||||
|
return new Text(theme.fg("muted", fallback), 0, 0);
|
||||||
|
}
|
||||||
|
const { kind, phase, toolName, body } = details;
|
||||||
|
const phaseTag = theme.fg("accent", `[${phase}]`);
|
||||||
|
const indent = " ".repeat(phase.length + 3);
|
||||||
|
let line: string;
|
||||||
|
switch (kind) {
|
||||||
|
case "tool-start": {
|
||||||
|
const arrow = theme.fg("muted", "→");
|
||||||
|
const name = theme.fg("toolTitle", theme.bold(toolName ?? ""));
|
||||||
|
const args = body ? ` ${theme.fg("muted", body)}` : "";
|
||||||
|
line = `${phaseTag} ${arrow} ${name}${args}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "tool-end": {
|
||||||
|
const arrow = theme.fg("success", "←");
|
||||||
|
const result = body
|
||||||
|
? ` ${theme.fg("dim", body)}`
|
||||||
|
: ` ${theme.fg("dim", "(ok)")}`;
|
||||||
|
line = `${indent}${arrow}${result}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "tool-error": {
|
||||||
|
const marker = theme.fg("error", "✗");
|
||||||
|
const result = body
|
||||||
|
? ` ${theme.fg("error", body)}`
|
||||||
|
: ` ${theme.fg("error", "failed")}`;
|
||||||
|
line = `${indent}${marker}${result}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "assistant":
|
||||||
|
line = `${phaseTag} ${theme.fg("muted", body ?? "")}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
line =
|
||||||
|
typeof message.content === "string"
|
||||||
|
? theme.fg("muted", message.content)
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
return new Text(line, 0, 0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ─── Reload detection: resume interrupted loops when session reloads ──
|
// ─── Reload detection: resume interrupted loops when session reloads ──
|
||||||
//
|
//
|
||||||
// ralpi runs task agent sessions in-process (createAgentSession), so they
|
// ralpi runs task agent sessions in-process (createAgentSession), so they
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { ProgressTracker } from "./progress";
|
|||||||
import type {
|
import type {
|
||||||
ExtensionContext,
|
ExtensionContext,
|
||||||
ModelRuntime,
|
ModelRuntime,
|
||||||
|
AgentSessionEvent,
|
||||||
} from "@earendil-works/pi-coding-agent";
|
} from "@earendil-works/pi-coding-agent";
|
||||||
import {
|
import {
|
||||||
buildTaskPrompt,
|
buildTaskPrompt,
|
||||||
@@ -56,6 +57,28 @@ import {
|
|||||||
} from "./utils";
|
} from "./utils";
|
||||||
import { updateTaskInFile } from "./parser";
|
import { updateTaskInFile } from "./parser";
|
||||||
|
|
||||||
|
// ─── Stream Forwarder (verbose chat style) ────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module-level callback for verbose per-event chat streaming. Set by
|
||||||
|
* `index.ts` at extension startup via {@link setStreamForwarder} when the
|
||||||
|
* config's `execution.chatStyle` is "verbose". `runTask`'s event callback
|
||||||
|
* checks this and forwards each tool_execution_start/end + message_end as
|
||||||
|
* its own chat message — the piolium/pygienium per-event stream. When null
|
||||||
|
* (compact mode, the default), only the completion message with its
|
||||||
|
* expandable tool-call tree shows.
|
||||||
|
*/
|
||||||
|
let _streamForwarder:
|
||||||
|
| ((phase: string, event: AgentSessionEvent) => void)
|
||||||
|
| null = null;
|
||||||
|
|
||||||
|
/** Register the verbose stream forwarder (called by index.ts at startup). */
|
||||||
|
export function setStreamForwarder(
|
||||||
|
fn: ((phase: string, event: AgentSessionEvent) => void) | null,
|
||||||
|
): void {
|
||||||
|
_streamForwarder = fn;
|
||||||
|
}
|
||||||
|
|
||||||
/** Optional callback to post a progress message into the chat history. */
|
/** Optional callback to post a progress message into the chat history. */
|
||||||
export type SendChatMessage = (
|
export type SendChatMessage = (
|
||||||
content: string,
|
content: string,
|
||||||
@@ -336,6 +359,10 @@ export async function runTask(
|
|||||||
projectDir,
|
projectDir,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
(event) => {
|
(event) => {
|
||||||
|
// Forward to the verbose stream when enabled.
|
||||||
|
if (_streamForwarder && config.execution.chatStyle === "verbose") {
|
||||||
|
_streamForwarder(`${task.id} · ${task.title}`, event);
|
||||||
|
}
|
||||||
if (event.type === "tool_execution_start") {
|
if (event.type === "tool_execution_start") {
|
||||||
const label = formatToolArg(event.toolName, event.args);
|
const label = formatToolArg(event.toolName, event.args);
|
||||||
toolCalls.push({
|
toolCalls.push({
|
||||||
|
|||||||
@@ -262,6 +262,12 @@ export interface RalpiConfig {
|
|||||||
* - "parallel": only when maxParallel > 1 and mode is parallel
|
* - "parallel": only when maxParallel > 1 and mode is parallel
|
||||||
* - "always": every task gets its own worktree */
|
* - "always": every task gets its own worktree */
|
||||||
worktrees: "always" | "parallel" | "never";
|
worktrees: "always" | "parallel" | "never";
|
||||||
|
/** Chat rendering style for tool calls during task execution.
|
||||||
|
* - "compact": single completion message per task with an expandable
|
||||||
|
* tool-call tree (collapsed shows last 3, expanded shows all).
|
||||||
|
* - "verbose": per-event stream — each tool start/end and assistant
|
||||||
|
* turn is its own chat line (piolium/pygienium-style). */
|
||||||
|
chatStyle: "compact" | "verbose";
|
||||||
};
|
};
|
||||||
prompts: {
|
prompts: {
|
||||||
/** Additional context injected into every task prompt */
|
/** Additional context injected into every task prompt */
|
||||||
@@ -310,6 +316,7 @@ export const DEFAULT_CONFIG: RalpiConfig = {
|
|||||||
loopTimeoutMs: 0, // 0 = no limit
|
loopTimeoutMs: 0, // 0 = no limit
|
||||||
worktrees: "parallel", // worktree isolation for parallel tasks by default
|
worktrees: "parallel", // worktree isolation for parallel tasks by default
|
||||||
maxSameModelAttempts: 5, // retry the same model up to 5 times before cycling to the next
|
maxSameModelAttempts: 5, // retry the same model up to 5 times before cycling to the next
|
||||||
|
chatStyle: "compact", // compact = completion message with tool-call tree; verbose = per-event stream
|
||||||
},
|
},
|
||||||
prompts: {
|
prompts: {
|
||||||
projectContext: "",
|
projectContext: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user