feat: Add timestamps to agent logs and fix horizontal scroll
- getAgentOutput now returns timestamped chunks ({ content, createdAt }[])
instead of a flat string, preserving DB chunk timestamps
- parseAgentOutput accepts TimestampedChunk[] and propagates timestamps
to each ParsedMessage
- AgentOutputViewer displays HH:MM:SS timestamps on text, tool_call,
system, and session_end messages
- Live subscription chunks get client-side Date.now() timestamps
- Fix horizontal scroll: overflow-x-hidden + break-words on content areas
- AgentLogsTab polls getTaskAgent every 5s until an agent is found,
then stops polling for live subscription to take over
- Fix slide-over layout: details tab scrolls independently, logs tab
fills remaining flex space for proper AgentOutputViewer sizing
This commit is contained in:
@@ -218,12 +218,15 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
|
|
||||||
getAgentOutput: publicProcedure
|
getAgentOutput: publicProcedure
|
||||||
.input(agentIdentifierSchema)
|
.input(agentIdentifierSchema)
|
||||||
.query(async ({ ctx, input }): Promise<string> => {
|
.query(async ({ ctx, input }) => {
|
||||||
const agent = await resolveAgent(ctx, input);
|
const agent = await resolveAgent(ctx, input);
|
||||||
const logChunkRepo = requireLogChunkRepository(ctx);
|
const logChunkRepo = requireLogChunkRepository(ctx);
|
||||||
|
|
||||||
const chunks = await logChunkRepo.findByAgentId(agent.id);
|
const chunks = await logChunkRepo.findByAgentId(agent.id);
|
||||||
return chunks.map(c => c.content).join('');
|
return chunks.map(c => ({
|
||||||
|
content: c.content,
|
||||||
|
createdAt: c.createdAt.toISOString(),
|
||||||
|
}));
|
||||||
}),
|
}),
|
||||||
|
|
||||||
onAgentOutput: publicProcedure
|
onAgentOutput: publicProcedure
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { trpc } from "@/lib/trpc";
|
|||||||
import { useSubscriptionWithErrorHandling } from "@/hooks";
|
import { useSubscriptionWithErrorHandling } from "@/hooks";
|
||||||
import {
|
import {
|
||||||
type ParsedMessage,
|
type ParsedMessage,
|
||||||
|
type TimestampedChunk,
|
||||||
getMessageStyling,
|
getMessageStyling,
|
||||||
parseAgentOutput,
|
parseAgentOutput,
|
||||||
} from "@/lib/parse-agent-output";
|
} from "@/lib/parse-agent-output";
|
||||||
@@ -21,8 +22,8 @@ export function AgentOutputViewer({ agentId, agentName, status, onStop }: AgentO
|
|||||||
const [messages, setMessages] = useState<ParsedMessage[]>([]);
|
const [messages, setMessages] = useState<ParsedMessage[]>([]);
|
||||||
const [follow, setFollow] = useState(true);
|
const [follow, setFollow] = useState(true);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
// Accumulate raw JSONL: initial query data + live subscription chunks
|
// Accumulate timestamped chunks: initial query data + live subscription chunks
|
||||||
const rawBufferRef = useRef<string>('');
|
const chunksRef = useRef<TimestampedChunk[]>([]);
|
||||||
|
|
||||||
// Load initial/historical output
|
// Load initial/historical output
|
||||||
const outputQuery = trpc.getAgentOutput.useQuery(
|
const outputQuery = trpc.getAgentOutput.useQuery(
|
||||||
@@ -40,8 +41,8 @@ export function AgentOutputViewer({ agentId, agentName, status, onStop }: AgentO
|
|||||||
// TrackedEnvelope shape: { id, data: { agentId, data: string } }
|
// TrackedEnvelope shape: { id, data: { agentId, data: string } }
|
||||||
const raw = event?.data?.data ?? event?.data;
|
const raw = event?.data?.data ?? event?.data;
|
||||||
const chunk = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
const chunk = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
||||||
rawBufferRef.current += chunk;
|
chunksRef.current = [...chunksRef.current, { content: chunk, createdAt: new Date().toISOString() }];
|
||||||
setMessages(parseAgentOutput(rawBufferRef.current));
|
setMessages(parseAgentOutput(chunksRef.current));
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error('Agent output subscription error:', error);
|
console.error('Agent output subscription error:', error);
|
||||||
@@ -54,14 +55,14 @@ export function AgentOutputViewer({ agentId, agentName, status, onStop }: AgentO
|
|||||||
// Set initial output when query loads
|
// Set initial output when query loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (outputQuery.data) {
|
if (outputQuery.data) {
|
||||||
rawBufferRef.current = outputQuery.data;
|
chunksRef.current = outputQuery.data;
|
||||||
setMessages(parseAgentOutput(outputQuery.data));
|
setMessages(parseAgentOutput(outputQuery.data));
|
||||||
}
|
}
|
||||||
}, [outputQuery.data]);
|
}, [outputQuery.data]);
|
||||||
|
|
||||||
// Reset output when agent changes
|
// Reset output when agent changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rawBufferRef.current = '';
|
chunksRef.current = [];
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
setFollow(true);
|
setFollow(true);
|
||||||
}, [agentId]);
|
}, [agentId]);
|
||||||
@@ -160,57 +161,64 @@ export function AgentOutputViewer({ agentId, agentName, status, onStop }: AgentO
|
|||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="flex-1 overflow-y-auto bg-terminal p-4"
|
className="flex-1 overflow-y-auto overflow-x-hidden bg-terminal p-4"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-terminal-muted text-sm">Loading output...</div>
|
<div className="text-terminal-muted text-sm">Loading output...</div>
|
||||||
) : !hasOutput ? (
|
) : !hasOutput ? (
|
||||||
<div className="text-terminal-muted text-sm">No output yet...</div>
|
<div className="text-terminal-muted text-sm">No output yet...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 min-w-0">
|
||||||
{messages.map((message, index) => (
|
{messages.map((message, index) => (
|
||||||
<div key={index} className={getMessageStyling(message.type)}>
|
<div key={index} className={getMessageStyling(message.type)}>
|
||||||
{message.type === 'system' && (
|
{message.type === 'system' && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="secondary" className="text-xs bg-terminal-border text-terminal-system">System</Badge>
|
<Badge variant="secondary" className="text-xs bg-terminal-border text-terminal-system">System</Badge>
|
||||||
<span className="text-xs text-terminal-muted">{message.content}</span>
|
<span className="text-xs text-terminal-muted">{message.content}</span>
|
||||||
|
<Timestamp date={message.timestamp} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{message.type === 'text' && (
|
{message.type === 'text' && (
|
||||||
<div className="font-mono text-sm whitespace-pre-wrap text-terminal-fg">
|
<>
|
||||||
{message.content}
|
<Timestamp date={message.timestamp} />
|
||||||
</div>
|
<div className="font-mono text-sm whitespace-pre-wrap break-words text-terminal-fg">
|
||||||
|
{message.content}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{message.type === 'tool_call' && (
|
{message.type === 'tool_call' && (
|
||||||
<div className="border-l-2 border-terminal-tool pl-3 py-1">
|
<div className="border-l-2 border-terminal-tool pl-3 py-1 min-w-0">
|
||||||
<Badge variant="default" className="mb-1 text-xs">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
{message.meta?.toolName}
|
<Badge variant="default" className="text-xs">
|
||||||
</Badge>
|
{message.meta?.toolName}
|
||||||
<div className="font-mono text-xs text-terminal-muted whitespace-pre-wrap">
|
</Badge>
|
||||||
|
<Timestamp date={message.timestamp} />
|
||||||
|
</div>
|
||||||
|
<div className="font-mono text-xs text-terminal-muted whitespace-pre-wrap break-words">
|
||||||
{message.content}
|
{message.content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{message.type === 'tool_result' && (
|
{message.type === 'tool_result' && (
|
||||||
<div className="border-l-2 border-terminal-result pl-3 py-1 bg-white/[0.02]">
|
<div className="border-l-2 border-terminal-result pl-3 py-1 bg-white/[0.02] min-w-0">
|
||||||
<Badge variant="outline" className="mb-1 text-xs text-terminal-result border-terminal-result">
|
<Badge variant="outline" className="mb-1 text-xs text-terminal-result border-terminal-result">
|
||||||
Result
|
Result
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className="font-mono text-xs text-terminal-muted whitespace-pre-wrap">
|
<div className="font-mono text-xs text-terminal-muted whitespace-pre-wrap break-words">
|
||||||
{message.content}
|
{message.content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{message.type === 'error' && (
|
{message.type === 'error' && (
|
||||||
<div className="border-l-2 border-terminal-error pl-3 py-1 bg-terminal-error/10">
|
<div className="border-l-2 border-terminal-error pl-3 py-1 bg-terminal-error/10 min-w-0">
|
||||||
<Badge variant="destructive" className="mb-1 text-xs">
|
<Badge variant="destructive" className="mb-1 text-xs">
|
||||||
Error
|
Error
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className="font-mono text-xs text-terminal-error whitespace-pre-wrap">
|
<div className="font-mono text-xs text-terminal-error whitespace-pre-wrap break-words">
|
||||||
{message.content}
|
{message.content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,6 +236,7 @@ export function AgentOutputViewer({ agentId, agentName, status, onStop }: AgentO
|
|||||||
{message.meta?.duration && (
|
{message.meta?.duration && (
|
||||||
<span className="text-xs text-terminal-muted">{(message.meta.duration / 1000).toFixed(1)}s</span>
|
<span className="text-xs text-terminal-muted">{(message.meta.duration / 1000).toFixed(1)}s</span>
|
||||||
)}
|
)}
|
||||||
|
<Timestamp date={message.timestamp} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -239,3 +248,16 @@ export function AgentOutputViewer({ agentId, agentName, status, onStop }: AgentO
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTime(date: Date): string {
|
||||||
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function Timestamp({ date }: { date?: Date }) {
|
||||||
|
if (!date) return null;
|
||||||
|
return (
|
||||||
|
<span className="shrink-0 text-[10px] text-terminal-muted/60 font-mono tabular-nums">
|
||||||
|
{formatTime(date)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export function TaskSlideOver({ onOpenChat }: TaskSlideOverProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className={cn("flex-1 min-h-0", tab === "details" ? "overflow-y-auto" : "flex flex-col")}>
|
||||||
{tab === "details" ? (
|
{tab === "details" ? (
|
||||||
<div className="px-5 py-4 space-y-5">
|
<div className="px-5 py-4 space-y-5">
|
||||||
{/* Metadata grid */}
|
{/* Metadata grid */}
|
||||||
@@ -337,7 +337,7 @@ export function TaskSlideOver({ onOpenChat }: TaskSlideOverProps) {
|
|||||||
function AgentLogsTab({ taskId }: { taskId: string }) {
|
function AgentLogsTab({ taskId }: { taskId: string }) {
|
||||||
const { data: agent, isLoading } = trpc.getTaskAgent.useQuery(
|
const { data: agent, isLoading } = trpc.getTaskAgent.useQuery(
|
||||||
{ taskId },
|
{ taskId },
|
||||||
{ refetchOnWindowFocus: false },
|
{ refetchOnWindowFocus: false, refetchInterval: (query) => query.state.data ? false : 5000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -357,7 +357,7 @@ function AgentLogsTab({ taskId }: { taskId: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<div className="flex-1 min-h-0">
|
||||||
<AgentOutputViewer
|
<AgentOutputViewer
|
||||||
agentId={agent.id}
|
agentId={agent.id}
|
||||||
agentName={agent.name ?? undefined}
|
agentName={agent.name ?? undefined}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface ParsedMessage {
|
|||||||
| "session_end"
|
| "session_end"
|
||||||
| "error";
|
| "error";
|
||||||
content: string;
|
content: string;
|
||||||
|
timestamp?: Date;
|
||||||
meta?: {
|
meta?: {
|
||||||
toolName?: string;
|
toolName?: string;
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
@@ -60,108 +61,135 @@ export function getMessageStyling(type: ParsedMessage["type"]): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseAgentOutput(raw: string): ParsedMessage[] {
|
/**
|
||||||
const lines = raw.split("\n").filter(Boolean);
|
* A chunk of raw JSONL content with an optional timestamp from the DB.
|
||||||
|
*/
|
||||||
|
export interface TimestampedChunk {
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse agent output. Accepts either a flat string (legacy) or timestamped chunks.
|
||||||
|
* When chunks have timestamps, each parsed message inherits the chunk's timestamp.
|
||||||
|
*/
|
||||||
|
export function parseAgentOutput(raw: string | TimestampedChunk[]): ParsedMessage[] {
|
||||||
|
const chunks: { content: string; timestamp?: Date }[] =
|
||||||
|
typeof raw === "string"
|
||||||
|
? [{ content: raw }]
|
||||||
|
: raw.map((c) => ({ content: c.content, timestamp: new Date(c.createdAt) }));
|
||||||
|
|
||||||
const parsedMessages: ParsedMessage[] = [];
|
const parsedMessages: ParsedMessage[] = [];
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const chunk of chunks) {
|
||||||
try {
|
const lines = chunk.content.split("\n").filter(Boolean);
|
||||||
const event = JSON.parse(line);
|
for (const line of lines) {
|
||||||
|
try {
|
||||||
|
const event = JSON.parse(line);
|
||||||
|
|
||||||
// System initialization
|
// System initialization
|
||||||
if (event.type === "system" && event.session_id) {
|
if (event.type === "system" && event.session_id) {
|
||||||
parsedMessages.push({
|
parsedMessages.push({
|
||||||
type: "system",
|
type: "system",
|
||||||
content: `Session started: ${event.session_id}`,
|
content: `Session started: ${event.session_id}`,
|
||||||
});
|
timestamp: chunk.timestamp,
|
||||||
}
|
});
|
||||||
|
|
||||||
// Assistant messages with text and tool calls
|
|
||||||
else if (
|
|
||||||
event.type === "assistant" &&
|
|
||||||
Array.isArray(event.message?.content)
|
|
||||||
) {
|
|
||||||
for (const block of event.message.content) {
|
|
||||||
if (block.type === "text" && block.text) {
|
|
||||||
parsedMessages.push({
|
|
||||||
type: "text",
|
|
||||||
content: block.text,
|
|
||||||
});
|
|
||||||
} else if (block.type === "tool_use") {
|
|
||||||
parsedMessages.push({
|
|
||||||
type: "tool_call",
|
|
||||||
content: formatToolCall(block),
|
|
||||||
meta: { toolName: block.name },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// User messages with tool results
|
// Assistant messages with text and tool calls
|
||||||
else if (
|
else if (
|
||||||
event.type === "user" &&
|
event.type === "assistant" &&
|
||||||
Array.isArray(event.message?.content)
|
Array.isArray(event.message?.content)
|
||||||
) {
|
) {
|
||||||
for (const block of event.message.content) {
|
for (const block of event.message.content) {
|
||||||
if (block.type === "tool_result") {
|
if (block.type === "text" && block.text) {
|
||||||
const rawContent = block.content;
|
|
||||||
const output =
|
|
||||||
typeof rawContent === "string"
|
|
||||||
? rawContent
|
|
||||||
: Array.isArray(rawContent)
|
|
||||||
? rawContent
|
|
||||||
.map((c: any) => c.text ?? JSON.stringify(c))
|
|
||||||
.join("\n")
|
|
||||||
: (event.tool_use_result?.stdout || "");
|
|
||||||
const stderr = event.tool_use_result?.stderr;
|
|
||||||
|
|
||||||
if (stderr) {
|
|
||||||
parsedMessages.push({
|
parsedMessages.push({
|
||||||
type: "error",
|
type: "text",
|
||||||
content: stderr,
|
content: block.text,
|
||||||
meta: { isError: true },
|
timestamp: chunk.timestamp,
|
||||||
});
|
});
|
||||||
} else if (output) {
|
} else if (block.type === "tool_use") {
|
||||||
const displayOutput =
|
|
||||||
output.length > 1000
|
|
||||||
? output.substring(0, 1000) + "\n... (truncated)"
|
|
||||||
: output;
|
|
||||||
parsedMessages.push({
|
parsedMessages.push({
|
||||||
type: "tool_result",
|
type: "tool_call",
|
||||||
content: displayOutput,
|
content: formatToolCall(block),
|
||||||
|
timestamp: chunk.timestamp,
|
||||||
|
meta: { toolName: block.name },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy streaming format
|
// User messages with tool results
|
||||||
else if (event.type === "stream_event" && event.event?.delta?.text) {
|
else if (
|
||||||
|
event.type === "user" &&
|
||||||
|
Array.isArray(event.message?.content)
|
||||||
|
) {
|
||||||
|
for (const block of event.message.content) {
|
||||||
|
if (block.type === "tool_result") {
|
||||||
|
const rawContent = block.content;
|
||||||
|
const output =
|
||||||
|
typeof rawContent === "string"
|
||||||
|
? rawContent
|
||||||
|
: Array.isArray(rawContent)
|
||||||
|
? rawContent
|
||||||
|
.map((c: any) => c.text ?? JSON.stringify(c))
|
||||||
|
.join("\n")
|
||||||
|
: (event.tool_use_result?.stdout || "");
|
||||||
|
const stderr = event.tool_use_result?.stderr;
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
parsedMessages.push({
|
||||||
|
type: "error",
|
||||||
|
content: stderr,
|
||||||
|
timestamp: chunk.timestamp,
|
||||||
|
meta: { isError: true },
|
||||||
|
});
|
||||||
|
} else if (output) {
|
||||||
|
const displayOutput =
|
||||||
|
output.length > 1000
|
||||||
|
? output.substring(0, 1000) + "\n... (truncated)"
|
||||||
|
: output;
|
||||||
|
parsedMessages.push({
|
||||||
|
type: "tool_result",
|
||||||
|
content: displayOutput,
|
||||||
|
timestamp: chunk.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy streaming format
|
||||||
|
else if (event.type === "stream_event" && event.event?.delta?.text) {
|
||||||
|
parsedMessages.push({
|
||||||
|
type: "text",
|
||||||
|
content: event.event.delta.text,
|
||||||
|
timestamp: chunk.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session completion
|
||||||
|
else if (event.type === "result") {
|
||||||
|
parsedMessages.push({
|
||||||
|
type: "session_end",
|
||||||
|
content: event.is_error ? "Session failed" : "Session completed",
|
||||||
|
timestamp: chunk.timestamp,
|
||||||
|
meta: {
|
||||||
|
isError: event.is_error,
|
||||||
|
cost: event.total_cost_usd,
|
||||||
|
duration: event.duration_ms,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not JSON, display as-is
|
||||||
parsedMessages.push({
|
parsedMessages.push({
|
||||||
type: "text",
|
type: "error",
|
||||||
content: event.event.delta.text,
|
content: line,
|
||||||
|
timestamp: chunk.timestamp,
|
||||||
|
meta: { isError: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session completion
|
|
||||||
else if (event.type === "result") {
|
|
||||||
parsedMessages.push({
|
|
||||||
type: "session_end",
|
|
||||||
content: event.is_error ? "Session failed" : "Session completed",
|
|
||||||
meta: {
|
|
||||||
isError: event.is_error,
|
|
||||||
cost: event.total_cost_usd,
|
|
||||||
duration: event.duration_ms,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Not JSON, display as-is
|
|
||||||
parsedMessages.push({
|
|
||||||
type: "error",
|
|
||||||
content: line,
|
|
||||||
meta: { isError: true },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parsedMessages;
|
return parsedMessages;
|
||||||
|
|||||||
@@ -153,9 +153,10 @@ Agent output is persisted to `agent_log_chunks` table and drives all live stream
|
|||||||
- DB insert → `agent:output` event emission (single source of truth for UI)
|
- DB insert → `agent:output` event emission (single source of truth for UI)
|
||||||
- No FK to agents — survives agent deletion
|
- No FK to agents — survives agent deletion
|
||||||
- Session tracking: spawn=1, resume=previousMax+1
|
- Session tracking: spawn=1, resume=previousMax+1
|
||||||
- Read path (`getAgentOutput` tRPC): concatenates all DB chunks (no file fallback)
|
- Read path (`getAgentOutput` tRPC): returns timestamped chunks `{ content, createdAt }[]` from DB
|
||||||
- Live path (`onAgentOutput` subscription): listens for `agent:output` events
|
- Live path (`onAgentOutput` subscription): listens for `agent:output` events (client stamps with `Date.now()`)
|
||||||
- Frontend: initial query loads from DB, subscription accumulates raw JSONL, both parsed via `parseAgentOutput()`
|
- Frontend: initial query loads timestamped chunks, subscription accumulates live chunks, both parsed via `parseAgentOutput()` which accepts `TimestampedChunk[]`
|
||||||
|
- Timestamps displayed inline (HH:MM:SS) on text, tool_call, system, and session_end messages
|
||||||
|
|
||||||
## Inter-Agent Communication
|
## Inter-Agent Communication
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
|
|||||||
| getAgent | query | Single agent by name or ID |
|
| getAgent | query | Single agent by name or ID |
|
||||||
| getAgentResult | query | Execution result |
|
| getAgentResult | query | Execution result |
|
||||||
| getAgentQuestions | query | Pending questions |
|
| getAgentQuestions | query | Pending questions |
|
||||||
| getAgentOutput | query | Full output from DB log chunks |
|
| getAgentOutput | query | Timestamped log chunks from DB (`{ content, createdAt }[]`) |
|
||||||
|
| getTaskAgent | query | Most recent agent assigned to a task (by taskId) |
|
||||||
| getActiveRefineAgent | query | Active refine agent for initiative |
|
| getActiveRefineAgent | query | Active refine agent for initiative |
|
||||||
| getActiveConflictAgent | query | Active conflict resolution agent for initiative (name starts with `conflict-`) |
|
| getActiveConflictAgent | query | Active conflict resolution agent for initiative (name starts with `conflict-`) |
|
||||||
| listWaitingAgents | query | Agents waiting for input |
|
| listWaitingAgents | query | Agents waiting for input |
|
||||||
|
|||||||
Reference in New Issue
Block a user