feat(KB-123): add agent role tracking and expanded logging to agent system

- Extend AgentLogEntry with agent field and new event types (thinking, tool_end)
- Expand AgentLogger with thinking, tool_end callbacks and agent role support
- Wire new logging callbacks in createKbAgent and all agent call-sites (executor, merger, reviewer, triage, pi)
- Update AgentLogViewer with agent role badges and rendering for new entry types
- Export AgentRole and AgentLogType from core package and add tests for new functionality
This commit is contained in:
Dustin Byrne
2026-03-27 01:19:42 -04:00
parent c8d6d29794
commit 82be575fc0
14 changed files with 474 additions and 66 deletions

View File

@@ -1,4 +1,4 @@
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, THINKING_LEVELS } from "./types.js";
export type { Column, Task, TaskAttachment, TaskCreateInput, TaskDetail, AgentLogEntry, BoardConfig, MergeResult, Settings, TaskStep, StepStatus, TaskLogEntry, ThinkingLevel } from "./types.js";
export type { Column, Task, TaskAttachment, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeResult, Settings, TaskStep, StepStatus, TaskLogEntry, ThinkingLevel } from "./types.js";
export { TaskStore } from "./store.js";
export { canTransition, getValidTransitions, resolveDependencyOrder } from "./board.js";

View File

@@ -610,6 +610,52 @@ describe("TaskStore", () => {
expect(logs).toHaveLength(5);
expect(logs[4].text).toBe("chunk 4");
});
it("appendAgentLog persists and reads back the agent field", async () => {
const task = await createTestTask();
await store.appendAgentLog(task.id, "hello", "text", undefined, "executor");
await store.appendAgentLog(task.id, "Read", "tool", "file.ts", "triage");
const logs = await store.getAgentLogs(task.id);
expect(logs).toHaveLength(2);
expect(logs[0].agent).toBe("executor");
expect(logs[1].agent).toBe("triage");
});
it("appendAgentLog omits agent field when not provided", async () => {
const task = await createTestTask();
await store.appendAgentLog(task.id, "hello", "text");
const logs = await store.getAgentLogs(task.id);
expect(logs).toHaveLength(1);
expect(logs[0]).not.toHaveProperty("agent");
});
it("new type values (thinking, tool_result, tool_error) round-trip correctly", async () => {
const task = await createTestTask();
await store.appendAgentLog(task.id, "internal thought", "thinking", undefined, "executor");
await store.appendAgentLog(task.id, "Bash", "tool_result", "output summary", "executor");
await store.appendAgentLog(task.id, "Read", "tool_error", "file not found", "reviewer");
const logs = await store.getAgentLogs(task.id);
expect(logs).toHaveLength(3);
expect(logs[0].type).toBe("thinking");
expect(logs[0].text).toBe("internal thought");
expect(logs[0].agent).toBe("executor");
expect(logs[1].type).toBe("tool_result");
expect(logs[1].text).toBe("Bash");
expect(logs[1].detail).toBe("output summary");
expect(logs[2].type).toBe("tool_error");
expect(logs[2].text).toBe("Read");
expect(logs[2].detail).toBe("file not found");
expect(logs[2].agent).toBe("reviewer");
});
});
describe("columnMovedAt", () => {

View File

@@ -882,17 +882,25 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
* Also emits an `agent:log` event for live streaming.
*
* @param taskId - The task ID (e.g. "KB-001")
* @param text - The text content (delta for "text", tool name for "tool")
* @param type - Whether this is a "text" delta or a "tool" invocation marker
* @param detail - Optional human-readable summary of tool args (e.g. file path, command)
* @param text - The text content (delta for "text"/"thinking", tool name for "tool"/"tool_result"/"tool_error")
* @param type - The entry type discriminator
* @param detail - Optional human-readable summary (tool args, result summary, or error message)
* @param agent - Optional agent role that produced this entry
*/
async appendAgentLog(taskId: string, text: string, type: "text" | "tool", detail?: string): Promise<void> {
async appendAgentLog(
taskId: string,
text: string,
type: AgentLogEntry["type"],
detail?: string,
agent?: AgentLogEntry["agent"],
): Promise<void> {
const entry: AgentLogEntry = {
timestamp: new Date().toISOString(),
taskId,
text,
type,
...(detail !== undefined && { detail }),
...(agent !== undefined && { agent }),
};
const dir = this.taskDir(taskId);
const logPath = join(dir, "agent.log");

View File

@@ -18,18 +18,27 @@ export interface TaskLogEntry {
outcome?: string;
}
/** A single chunk of agent output (text delta or tool invocation) persisted to disk. */
/** The set of agent roles that produce log entries. */
export type AgentRole = "triage" | "executor" | "reviewer" | "merger";
/** The discriminator for agent log entry types. */
export type AgentLogType = "text" | "tool" | "thinking" | "tool_result" | "tool_error";
/** A single chunk of agent output persisted to disk (JSONL in agent.log). */
export interface AgentLogEntry {
/** ISO-8601 timestamp of when the entry was recorded */
/** ISO-8601 timestamp of when the entry was recorded. */
timestamp: string;
/** The task this log entry belongs to */
/** The task this log entry belongs to. */
taskId: string;
/** The text content (delta for "text", tool name for "tool") */
/** The text content (delta for "text"/"thinking", tool name for "tool"/"tool_result"/"tool_error"). */
text: string;
/** Whether this is a text delta or a tool invocation marker */
type: "text" | "tool";
/** For tool entries: human-readable summary of tool args (e.g. file path, command) */
/** The kind of entry — text delta, tool invocation marker, thinking block, tool result, or tool error. */
type: AgentLogType;
/** For tool entries: human-readable summary of tool args (e.g. file path, command).
* For tool_result/tool_error: summary of the result or error message. */
detail?: string;
/** Which agent produced this entry. Absent in logs written before this field was added. */
agent?: AgentRole;
}
export interface TaskAttachment {