Add MarkdownView, task retry endpoint, and chat/model store improvements
Adds MarkdownView component to design system, task retry API route, markdown rendering in dashboard and chat, and model store enhancements. Includes DB migration for schema updates.
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
type ProviderMessages,
|
||||
} from "@/lib/chat-providers";
|
||||
import { setupChatWorkspace, chatWorkspacePath } from "@/lib/chat-workspace";
|
||||
import { recordUsage } from "@/lib/model-store";
|
||||
import { db } from "@/lib/db";
|
||||
import { projects } from "@homelab/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -156,6 +157,10 @@ export async function POST(request: NextRequest) {
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
|
||||
}
|
||||
|
||||
const streamStart = Date.now();
|
||||
let totalInputTokens = 0;
|
||||
let totalOutputTokens = 0;
|
||||
|
||||
try {
|
||||
let currentMessages = [...providerMessages];
|
||||
let rounds = 0;
|
||||
@@ -182,6 +187,10 @@ export async function POST(request: NextRequest) {
|
||||
send(event);
|
||||
} else if (event.type === "message_end") {
|
||||
finalUsage = event.usage;
|
||||
if (event.usage) {
|
||||
totalInputTokens += event.usage.input;
|
||||
totalOutputTokens += event.usage.output;
|
||||
}
|
||||
} else if (event.type === "error") {
|
||||
send(event);
|
||||
controller.close();
|
||||
@@ -228,6 +237,19 @@ export async function POST(request: NextRequest) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "error", message: msg })}\n\n`));
|
||||
} finally {
|
||||
// Persist chat usage to DB
|
||||
if (totalInputTokens > 0 || totalOutputTokens > 0) {
|
||||
recordUsage({
|
||||
source: "chat",
|
||||
modelId: model,
|
||||
provider,
|
||||
conversationId: conversationId ?? undefined,
|
||||
inputTokens: totalInputTokens,
|
||||
outputTokens: totalOutputTokens,
|
||||
durationMs: Date.now() - streamStart,
|
||||
timestamp: Date.now(),
|
||||
}).catch(err => console.error("[chat] Failed to record usage:", err));
|
||||
}
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
|
||||
34
apps/harness/src/app/api/tasks/[id]/retry/route.ts
Normal file
34
apps/harness/src/app/api/tasks/[id]/retry/route.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getTask, updateTask } from "@/lib/store";
|
||||
import { startOrchestrator } from "@/lib/orchestrator";
|
||||
|
||||
export async function POST(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { id } = await params;
|
||||
const task = await getTask(id);
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: "Task not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (task.status !== "failed" && task.status !== "completed") {
|
||||
return NextResponse.json(
|
||||
{ error: `Task is ${task.status}, can only retry failed or completed tasks` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Reset task to pending so the orchestrator picks it up again
|
||||
await updateTask(id, {
|
||||
status: "pending",
|
||||
iteration: 0,
|
||||
startedAt: null,
|
||||
evals: {},
|
||||
} as any);
|
||||
|
||||
await startOrchestrator();
|
||||
|
||||
return NextResponse.json({ ok: true, message: "Task reset to pending and orchestrator started" });
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Mono,
|
||||
Btn,
|
||||
SearchableDropdown,
|
||||
MarkdownView,
|
||||
type DropdownOption,
|
||||
} from "./harness-design-system";
|
||||
import type { ChatMessage, ToolCallRecord, StreamEvent, Conversation } from "@/lib/chat-types";
|
||||
@@ -556,40 +557,35 @@ export default function ChatTab({ mobile, projects }: { mobile: boolean; project
|
||||
))}
|
||||
|
||||
{/* Streaming message */}
|
||||
{streaming && (streamText || streamToolCalls.length > 0) && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: tokens.space[2] }}>
|
||||
<div
|
||||
style={{
|
||||
background: tokens.color.bg1,
|
||||
border: `1px solid ${tokens.color.border0}`,
|
||||
padding: `${tokens.space[3]}px ${tokens.space[4]}px`,
|
||||
maxWidth: "85%",
|
||||
}}
|
||||
>
|
||||
{streamToolCalls.length > 0 && (
|
||||
<div style={{ marginBottom: streamText ? tokens.space[3] : 0 }}>
|
||||
{streamToolCalls.map((tc) => (
|
||||
<ToolCallBlock key={tc.id} toolCall={tc} pending={pendingToolIds.has(tc.id)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{streamText && (
|
||||
<div style={{ fontFamily: tokens.font.mono, fontSize: tokens.size.sm, color: tokens.color.text1, whiteSpace: "pre-wrap", wordBreak: "break-word", lineHeight: 1.6 }}>
|
||||
{streamText}
|
||||
<span style={{ display: "inline-block", width: 6, height: 14, background: tokens.color.accent, marginLeft: 2, animation: "hpulse 1s infinite" }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Thinking indicator */}
|
||||
{thinking && streaming && (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: tokens.space[2], padding: `${tokens.space[2]}px 0` }}>
|
||||
<span style={{ display: "inline-block", width: 6, height: 6, borderRadius: "50%", background: tokens.color.accent, animation: "hpulse 1.5s infinite", boxShadow: tokens.color.accentGlow }} />
|
||||
<Mono size={tokens.size.sm} color={tokens.color.text2} style={{ animation: "hpulse 1.5s infinite" }}>
|
||||
Thinking...
|
||||
</Mono>
|
||||
{streaming && (streamText || streamToolCalls.length > 0 || thinking) && (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: tokens.space[2] }}>
|
||||
{/* Tool use ticker — compact inline summary */}
|
||||
{streamToolCalls.length > 0 && (
|
||||
<ToolTicker toolCalls={streamToolCalls} pendingIds={pendingToolIds} />
|
||||
)}
|
||||
{/* Streamed text with markdown */}
|
||||
{streamText && (
|
||||
<div
|
||||
style={{
|
||||
background: tokens.color.bg1,
|
||||
border: `1px solid ${tokens.color.border0}`,
|
||||
padding: `${tokens.space[3]}px ${tokens.space[4]}px`,
|
||||
maxWidth: "85%",
|
||||
}}
|
||||
>
|
||||
<MarkdownView content={streamText} />
|
||||
<span style={{ display: "inline-block", width: 6, height: 14, background: tokens.color.accent, marginLeft: 2, animation: "hpulse 1s infinite", verticalAlign: "text-bottom" }} />
|
||||
</div>
|
||||
)}
|
||||
{/* Thinking indicator */}
|
||||
{thinking && !streamText && (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: tokens.space[2], padding: `${tokens.space[2]}px 0` }}>
|
||||
<span style={{ display: "inline-block", width: 6, height: 6, borderRadius: "50%", background: tokens.color.accent, animation: "hpulse 1.5s infinite", boxShadow: tokens.color.accentGlow }} />
|
||||
<Mono size={tokens.size.sm} color={tokens.color.text2} style={{ animation: "hpulse 1.5s infinite" }}>
|
||||
Thinking...
|
||||
</Mono>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -665,6 +661,10 @@ function MessageBubble({ message }: { message: ChatMessage }) {
|
||||
const isUser = message.role === "user";
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: isUser ? "flex-end" : "flex-start", gap: tokens.space[1] }}>
|
||||
{/* Compact tool summary for saved messages */}
|
||||
{!isUser && message.toolCalls && message.toolCalls.length > 0 && (
|
||||
<ToolSummary toolCalls={message.toolCalls} />
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
background: isUser ? tokens.color.bg2 : tokens.color.bg1,
|
||||
@@ -673,16 +673,13 @@ function MessageBubble({ message }: { message: ChatMessage }) {
|
||||
maxWidth: "85%",
|
||||
}}
|
||||
>
|
||||
{message.toolCalls && message.toolCalls.length > 0 && (
|
||||
<div style={{ marginBottom: message.content ? tokens.space[3] : 0 }}>
|
||||
{message.toolCalls.map((tc) => (
|
||||
<ToolCallBlock key={tc.id} toolCall={tc} pending={false} />
|
||||
))}
|
||||
{isUser ? (
|
||||
<div style={{ fontFamily: tokens.font.mono, fontSize: tokens.size.sm, color: tokens.color.text1, whiteSpace: "pre-wrap", wordBreak: "break-word", lineHeight: 1.6 }}>
|
||||
{message.content}
|
||||
</div>
|
||||
) : (
|
||||
<MarkdownView content={message.content} />
|
||||
)}
|
||||
<div style={{ fontFamily: tokens.font.mono, fontSize: tokens.size.sm, color: tokens.color.text1, whiteSpace: "pre-wrap", wordBreak: "break-word", lineHeight: 1.6 }}>
|
||||
{message.content}
|
||||
</div>
|
||||
</div>
|
||||
{message.model && (
|
||||
<Label color={tokens.color.text3} style={{ fontSize: 11 }}>{message.model}</Label>
|
||||
@@ -691,30 +688,104 @@ function MessageBubble({ message }: { message: ChatMessage }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tool Call Block ──────────────────────────────────────────
|
||||
// ── Tool Ticker (live streaming) ─────────────────────────────
|
||||
|
||||
function ToolTicker({ toolCalls, pendingIds }: { toolCalls: ToolCallRecord[]; pendingIds: Set<string> }) {
|
||||
const completed = toolCalls.filter(tc => !pendingIds.has(tc.id));
|
||||
const running = toolCalls.filter(tc => pendingIds.has(tc.id));
|
||||
const totalMs = completed.reduce((sum, tc) => sum + tc.durationMs, 0);
|
||||
|
||||
function ToolCallBlock({ toolCall, pending }: { toolCall: ToolCallRecord; pending: boolean }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
return (
|
||||
<div style={{ border: `1px solid ${pending ? tokens.color.warnDim : tokens.color.border0}`, background: tokens.color.bg0, marginBottom: tokens.space[2] }}>
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", gap: tokens.space[2], flexWrap: "wrap",
|
||||
padding: `${tokens.space[1]}px ${tokens.space[2]}px`,
|
||||
background: tokens.color.bg1, border: `1px solid ${tokens.color.border0}`,
|
||||
maxWidth: "85%",
|
||||
}}>
|
||||
{running.length > 0 && (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: tokens.space[1] }}>
|
||||
<span style={{ display: "inline-block", width: 5, height: 5, borderRadius: "50%", background: tokens.color.warn, animation: "hpulse 1s infinite" }} />
|
||||
<Label color={tokens.color.warn} style={{ fontSize: 11 }}>
|
||||
{running[running.length - 1].name}
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
{completed.length > 0 && (
|
||||
<Label color={tokens.color.text3} style={{ fontSize: 11 }}>
|
||||
{completed.length} tool{completed.length !== 1 ? "s" : ""} · {totalMs > 1000 ? `${(totalMs / 1000).toFixed(1)}s` : `${totalMs}ms`}
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tool Summary (saved messages — expandable) ───────────────
|
||||
|
||||
function ToolSummary({ toolCalls }: { toolCalls: ToolCallRecord[] }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const totalMs = toolCalls.reduce((sum, tc) => sum + tc.durationMs, 0);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: "85%" }}>
|
||||
<div
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
style={{ display: "flex", alignItems: "center", gap: tokens.space[2], padding: `${tokens.space[1]}px ${tokens.space[3]}px`, cursor: "pointer", minHeight: 32 }}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", gap: tokens.space[2],
|
||||
padding: `${tokens.space[1]}px ${tokens.space[2]}px`,
|
||||
cursor: "pointer", userSelect: "none",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: tokens.color.text3, fontSize: tokens.size.xs, fontFamily: tokens.font.mono }}>
|
||||
{expanded ? "▾" : "▸"}
|
||||
</span>
|
||||
<Label color={pending ? tokens.color.warn : tokens.color.purple}>{toolCall.name}</Label>
|
||||
{pending && <Label color={tokens.color.warn} style={{ fontSize: 11 }}>running...</Label>}
|
||||
{!pending && toolCall.durationMs > 0 && (
|
||||
<Label color={tokens.color.text3} style={{ fontSize: 11 }}>{toolCall.durationMs}ms</Label>
|
||||
<Label color={tokens.color.purple} style={{ fontSize: 11 }}>
|
||||
{toolCalls.length} tool{toolCalls.length !== 1 ? "s" : ""} used
|
||||
</Label>
|
||||
<Label color={tokens.color.text3} style={{ fontSize: 11 }}>
|
||||
{totalMs > 1000 ? `${(totalMs / 1000).toFixed(1)}s` : `${totalMs}ms`}
|
||||
</Label>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div style={{
|
||||
background: tokens.color.bg0, border: `1px solid ${tokens.color.border0}`,
|
||||
marginTop: tokens.space[1],
|
||||
}}>
|
||||
{toolCalls.map((tc) => (
|
||||
<ToolCallDetail key={tc.id} toolCall={tc} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tool Call Detail (individual expandable row) ─────────────
|
||||
|
||||
function ToolCallDetail({ toolCall }: { toolCall: ToolCallRecord }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
return (
|
||||
<div style={{ borderBottom: `1px solid ${tokens.color.border0}` }}>
|
||||
<div
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", gap: tokens.space[2],
|
||||
padding: `${tokens.space[1]}px ${tokens.space[3]}px`,
|
||||
cursor: "pointer", minHeight: 28,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: tokens.color.text3, fontSize: 10, fontFamily: tokens.font.mono }}>
|
||||
{expanded ? "▾" : "▸"}
|
||||
</span>
|
||||
<Label color={tokens.color.purple} style={{ fontSize: 11 }}>{toolCall.name}</Label>
|
||||
{toolCall.durationMs > 0 && (
|
||||
<Label color={tokens.color.text3} style={{ fontSize: 11, marginLeft: "auto" }}>{toolCall.durationMs}ms</Label>
|
||||
)}
|
||||
</div>
|
||||
{expanded && (
|
||||
<div style={{ borderTop: `1px solid ${tokens.color.border0}`, padding: `${tokens.space[2]}px ${tokens.space[3]}px` }}>
|
||||
{Object.keys(toolCall.input).length > 0 && (
|
||||
<div style={{ marginBottom: tokens.space[2] }}>
|
||||
<Label color={tokens.color.text3}>Input</Label>
|
||||
<Label color={tokens.color.text3} style={{ fontSize: 10 }}>INPUT</Label>
|
||||
<pre style={{ fontFamily: tokens.font.mono, fontSize: tokens.size.xs, color: tokens.color.text2, margin: `${tokens.space[1]}px 0`, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
|
||||
{JSON.stringify(toolCall.input, null, 2)}
|
||||
</pre>
|
||||
@@ -722,7 +793,7 @@ function ToolCallBlock({ toolCall, pending }: { toolCall: ToolCallRecord; pendin
|
||||
)}
|
||||
{toolCall.output && (
|
||||
<div>
|
||||
<Label color={tokens.color.text3}>Output</Label>
|
||||
<Label color={tokens.color.text3} style={{ fontSize: 10 }}>OUTPUT</Label>
|
||||
<pre style={{ fontFamily: tokens.font.mono, fontSize: tokens.size.xs, color: tokens.color.text2, margin: `${tokens.space[1]}px 0`, whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: 200, overflowY: "auto" }}>
|
||||
{toolCall.output}
|
||||
</pre>
|
||||
|
||||
@@ -318,10 +318,14 @@ function LoopsTab({ tasks, setTasks, mobile }: { tasks: Task[]; setTasks: React.
|
||||
|
||||
const handleStart = async (id: string) => {
|
||||
await fetch(`/api/tasks/${encodeURIComponent(id)}/start`, { method: "POST" }).catch(() => {});
|
||||
// Optimistic update — the orchestrator will pick it up
|
||||
setTasks(prev => prev.map(t => t.id === id ? { ...t, status: "running", startedAt: Date.now() } : t));
|
||||
};
|
||||
|
||||
const handleRetry = async (id: string) => {
|
||||
await fetch(`/api/tasks/${encodeURIComponent(id)}/retry`, { method: "POST" }).catch(() => {});
|
||||
setTasks(prev => prev.map(t => t.id === id ? { ...t, status: "pending", iteration: 0, startedAt: null, evals: {}, iterations: [] } : t));
|
||||
};
|
||||
|
||||
const TaskRow = ({ task }: { task: Task }) => {
|
||||
const s = STATUS[task.status] || STATUS.pending;
|
||||
return (
|
||||
@@ -343,6 +347,9 @@ function LoopsTab({ tasks, setTasks, mobile }: { tasks: Task[]; setTasks: React.
|
||||
{task.status === "pending" && (
|
||||
<Btn variant="primary" onClick={(e) => { e.stopPropagation(); handleStart(task.id); }} style={{ fontSize: tokens.size.xs, padding: "0 8px", minHeight: 28 }}>START</Btn>
|
||||
)}
|
||||
{task.status === "failed" && (
|
||||
<Btn variant="primary" onClick={(e) => { e.stopPropagation(); handleRetry(task.id); }} style={{ fontSize: tokens.size.xs, padding: "0 8px", minHeight: 28 }}>RETRY</Btn>
|
||||
)}
|
||||
{mobile && <span style={{ color: tokens.color.text3, fontSize: 16 }}>›</span>}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2003,14 +2010,34 @@ export default function HarnessDashboard() {
|
||||
if (activeTab === "KNOWLEDGE") loadKnowledge();
|
||||
}, [activeTab]);
|
||||
|
||||
const handleNewTask = (form: TaskForm) => {
|
||||
const handleNewTask = async (form: TaskForm) => {
|
||||
const proj = projects.find(p => p.id === form.projectId);
|
||||
setTasks(prev => [...prev, {
|
||||
id: `task-${Date.now()}`, slug: form.slug, goal: form.goal,
|
||||
project: proj?.name || "—", status: "pending",
|
||||
iteration: 0, maxIterations: parseInt(form.maxIterations) || 6,
|
||||
startedAt: null, evals: {}, iterations: [],
|
||||
}]);
|
||||
// Use the first workspace repo (owner/repo format) as the project for git operations
|
||||
const projectRepo = proj?.workspaces?.[0]?.repo || proj?.name || "—";
|
||||
|
||||
const spec = {
|
||||
slug: form.slug,
|
||||
goal: form.goal,
|
||||
project: projectRepo,
|
||||
agentId: form.agentId,
|
||||
maxIterations: parseInt(form.maxIterations) || 6,
|
||||
criteria: form.criteria.filter(c => c.label && c.target),
|
||||
constraints: form.constraints.filter(Boolean),
|
||||
knowledgeRefs: form.knowledgeRefs,
|
||||
gitProvider: "gitea" as const,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/tasks", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(spec),
|
||||
});
|
||||
if (res.ok) {
|
||||
const task = await res.json();
|
||||
setTasks(prev => [...prev, task]);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
setActiveTab("LOOPS");
|
||||
};
|
||||
|
||||
|
||||
@@ -539,3 +539,71 @@ export function BackBtn({ onBack, label }: { onBack: () => void; label: string }
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Markdown ─────────────────────────────────────────────────
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
export function renderMarkdown(md: string): string {
|
||||
let html = escapeHtml(md);
|
||||
|
||||
// Code blocks (``` ... ```)
|
||||
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, _lang, code) =>
|
||||
`<pre style="background:${tokens.color.bg0};border:1px solid ${tokens.color.border0};padding:${tokens.space[3]}px;overflow-x:auto;margin:${tokens.space[3]}px 0"><code>${code}</code></pre>`
|
||||
);
|
||||
|
||||
// Inline code
|
||||
html = html.replace(/`([^`]+)`/g, (_m, code) =>
|
||||
`<code style="background:${tokens.color.bg0};border:1px solid ${tokens.color.border0};padding:1px 4px">${code}</code>`
|
||||
);
|
||||
|
||||
// Headings
|
||||
html = html.replace(/^#### (.+)$/gm, `<h4 style="color:${tokens.color.text0};font-size:${tokens.size.base}px;margin:${tokens.space[4]}px 0 ${tokens.space[2]}px;font-weight:500">$1</h4>`);
|
||||
html = html.replace(/^### (.+)$/gm, `<h3 style="color:${tokens.color.text0};font-size:${tokens.size.md}px;margin:${tokens.space[5]}px 0 ${tokens.space[2]}px;font-weight:500">$1</h3>`);
|
||||
html = html.replace(/^## (.+)$/gm, `<h2 style="color:${tokens.color.text0};font-size:${tokens.size.lg}px;margin:${tokens.space[6]}px 0 ${tokens.space[3]}px;font-weight:500">$1</h2>`);
|
||||
html = html.replace(/^# (.+)$/gm, `<h1 style="color:${tokens.color.text0};font-size:${tokens.size.xl}px;margin:${tokens.space[6]}px 0 ${tokens.space[3]}px;font-weight:400">$1</h1>`);
|
||||
|
||||
// Bold / italic
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, `<strong style="color:${tokens.color.text0}">$1</strong>`);
|
||||
html = html.replace(/\*(.+?)\*/g, `<em>$1</em>`);
|
||||
|
||||
// Ordered lists
|
||||
html = html.replace(/^(\d+)\. (.+)$/gm, `<li style="margin-left:${tokens.space[4]}px;list-style:decimal">$2</li>`);
|
||||
|
||||
// Unordered lists
|
||||
html = html.replace(/^- (.+)$/gm, `<li style="margin-left:${tokens.space[4]}px;list-style:disc">$1</li>`);
|
||||
|
||||
// Horizontal rules
|
||||
html = html.replace(/^---$/gm, `<hr style="border:none;border-top:1px solid ${tokens.color.border0};margin:${tokens.space[4]}px 0" />`);
|
||||
|
||||
// Links
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, `<a href="$2" style="color:${tokens.color.accent};text-decoration:none" target="_blank" rel="noopener">$1</a>`);
|
||||
|
||||
// Paragraphs (double newline)
|
||||
html = html.replace(/\n\n/g, `</p><p style="margin:${tokens.space[3]}px 0">`);
|
||||
html = `<p style="margin:${tokens.space[3]}px 0">${html}</p>`;
|
||||
|
||||
// Single newlines as <br> (within paragraphs)
|
||||
html = html.replace(/\n/g, "<br />");
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
export function MarkdownView({ content, style }: { content: string; style?: React.CSSProperties }) {
|
||||
const html = renderMarkdown(content);
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: tokens.size.sm,
|
||||
color: tokens.color.text1,
|
||||
lineHeight: 1.8,
|
||||
...style,
|
||||
}}
|
||||
className="md-content"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ function rowToConversation(row: ConvRow): Conversation {
|
||||
provider: row.provider,
|
||||
workspace: row.workspace ?? undefined,
|
||||
messages: (row.messages ?? []) as ChatMessage[],
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface Conversation {
|
||||
model: string;
|
||||
provider: string;
|
||||
workspace?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/** Wire format for messages sent to/from the API route */
|
||||
|
||||
@@ -3,11 +3,13 @@ import { modelUsage as usageTable } from "@homelab/db";
|
||||
import { KNOWN_MODELS } from "./model-providers";
|
||||
|
||||
export interface ModelUsageEntry {
|
||||
source?: "task" | "chat";
|
||||
modelId: string;
|
||||
provider: string;
|
||||
taskId: string;
|
||||
taskSlug: string;
|
||||
iteration: number;
|
||||
taskId?: string;
|
||||
taskSlug?: string;
|
||||
iteration?: number;
|
||||
conversationId?: string;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
durationMs: number;
|
||||
@@ -28,11 +30,13 @@ export interface ModelUsageSummary {
|
||||
|
||||
export async function recordUsage(entry: ModelUsageEntry): Promise<void> {
|
||||
await db.insert(usageTable).values({
|
||||
source: entry.source ?? "task",
|
||||
modelId: entry.modelId,
|
||||
provider: entry.provider,
|
||||
taskId: entry.taskId,
|
||||
taskSlug: entry.taskSlug,
|
||||
iteration: entry.iteration,
|
||||
taskId: entry.taskId ?? null,
|
||||
taskSlug: entry.taskSlug ?? null,
|
||||
iteration: entry.iteration ?? null,
|
||||
conversationId: entry.conversationId ?? null,
|
||||
inputTokens: entry.inputTokens,
|
||||
outputTokens: entry.outputTokens,
|
||||
durationMs: entry.durationMs,
|
||||
@@ -40,14 +44,30 @@ export async function recordUsage(entry: ModelUsageEntry): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUsageLog(): Promise<ModelUsageEntry[]> {
|
||||
export interface UsageLogEntry {
|
||||
source: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
taskId?: string;
|
||||
taskSlug?: string;
|
||||
iteration?: number;
|
||||
conversationId?: string;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
durationMs: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export async function getUsageLog(): Promise<UsageLogEntry[]> {
|
||||
const rows = await db.select().from(usageTable);
|
||||
return rows.map(r => ({
|
||||
source: r.source,
|
||||
modelId: r.modelId,
|
||||
provider: r.provider,
|
||||
taskId: r.taskId,
|
||||
taskSlug: r.taskSlug,
|
||||
iteration: r.iteration,
|
||||
taskId: r.taskId ?? undefined,
|
||||
taskSlug: r.taskSlug ?? undefined,
|
||||
iteration: r.iteration ?? undefined,
|
||||
conversationId: r.conversationId ?? undefined,
|
||||
inputTokens: r.inputTokens,
|
||||
outputTokens: r.outputTokens,
|
||||
durationMs: r.durationMs,
|
||||
|
||||
@@ -159,6 +159,9 @@ export async function updateTask(id: string, updates: Partial<Task>): Promise<Ta
|
||||
if (updates.evals !== undefined) dbUpdates.evals = updates.evals;
|
||||
if (updates.pr !== undefined) dbUpdates.pr = updates.pr;
|
||||
|
||||
// Reset cancel flag when status changes to pending (retry)
|
||||
if (updates.status === "pending") dbUpdates.cancelRequested = false;
|
||||
|
||||
await db.update(tasksTable).set(dbUpdates).where(eq(tasksTable.id, id));
|
||||
return getTask(id);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
5
packages/db/drizzle/0006_tiny_lockjaw.sql
Normal file
5
packages/db/drizzle/0006_tiny_lockjaw.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE "harness_model_usage" ALTER COLUMN "task_id" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "harness_model_usage" ALTER COLUMN "task_slug" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "harness_model_usage" ALTER COLUMN "iteration" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "harness_model_usage" ADD COLUMN "source" text DEFAULT 'task' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "harness_model_usage" ADD COLUMN "conversation_id" text;
|
||||
837
packages/db/drizzle/meta/0006_snapshot.json
Normal file
837
packages/db/drizzle/meta/0006_snapshot.json
Normal file
@@ -0,0 +1,837 @@
|
||||
{
|
||||
"id": "640ba87d-2722-431b-a2da-2400c5bbd77c",
|
||||
"prevId": "2ecc8bb5-604b-4c54-89a6-d9e0ff7e88a6",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.harness_agent_configs": {
|
||||
"name": "harness_agent_configs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"runtime": {
|
||||
"name": "runtime",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"model_id": {
|
||||
"name": "model_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "max_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"env": {
|
||||
"name": "env",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_chat_conversations": {
|
||||
"name": "harness_chat_conversations",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"workspace": {
|
||||
"name": "workspace",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"messages": {
|
||||
"name": "messages",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'[]'::jsonb"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_credentials": {
|
||||
"name": "harness_credentials",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"base_url": {
|
||||
"name": "base_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"usage": {
|
||||
"name": "usage",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'any'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_event_log": {
|
||||
"name": "harness_event_log",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"trigger_id": {
|
||||
"name": "trigger_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"delivery_id": {
|
||||
"name": "delivery_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"event_type": {
|
||||
"name": "event_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"repo": {
|
||||
"name": "repo",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"commit_sha": {
|
||||
"name": "commit_sha",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"branch": {
|
||||
"name": "branch",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'received'"
|
||||
},
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"skip_reason": {
|
||||
"name": "skip_reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payload": {
|
||||
"name": "payload",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"harness_event_log_delivery_id_unique": {
|
||||
"name": "harness_event_log_delivery_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"delivery_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_event_triggers": {
|
||||
"name": "harness_event_triggers",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"event_type": {
|
||||
"name": "event_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"repo_filter": {
|
||||
"name": "repo_filter",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"state_filter": {
|
||||
"name": "state_filter",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"context_filter": {
|
||||
"name": "context_filter",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"task_template": {
|
||||
"name": "task_template",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"consecutive_failures": {
|
||||
"name": "consecutive_failures",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"max_consecutive_failures": {
|
||||
"name": "max_consecutive_failures",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 3
|
||||
},
|
||||
"disabled_reason": {
|
||||
"name": "disabled_reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"webhook_secret": {
|
||||
"name": "webhook_secret",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_iterations": {
|
||||
"name": "harness_iterations",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"n": {
|
||||
"name": "n",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"diagnosis": {
|
||||
"name": "diagnosis",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"agent_output": {
|
||||
"name": "agent_output",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"evals": {
|
||||
"name": "evals",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"diff_stats": {
|
||||
"name": "diff_stats",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"completed_at": {
|
||||
"name": "completed_at",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_model_usage": {
|
||||
"name": "harness_model_usage",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"source": {
|
||||
"name": "source",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'task'"
|
||||
},
|
||||
"model_id": {
|
||||
"name": "model_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"task_slug": {
|
||||
"name": "task_slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"iteration": {
|
||||
"name": "iteration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"conversation_id": {
|
||||
"name": "conversation_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"duration_ms": {
|
||||
"name": "duration_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_orchestrator": {
|
||||
"name": "harness_orchestrator",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "'singleton'"
|
||||
},
|
||||
"running": {
|
||||
"name": "running",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"current_task_id": {
|
||||
"name": "current_task_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"heartbeat": {
|
||||
"name": "heartbeat",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_projects": {
|
||||
"name": "harness_projects",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspaces": {
|
||||
"name": "workspaces",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'[]'::jsonb"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.harness_tasks": {
|
||||
"name": "harness_tasks",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"goal": {
|
||||
"name": "goal",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"iteration": {
|
||||
"name": "iteration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"max_iterations": {
|
||||
"name": "max_iterations",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 6
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"completed_at": {
|
||||
"name": "completed_at",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"project": {
|
||||
"name": "project",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'—'"
|
||||
},
|
||||
"evals": {
|
||||
"name": "evals",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"pr": {
|
||||
"name": "pr",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cancel_requested": {
|
||||
"name": "cancel_requested",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"spec": {
|
||||
"name": "spec",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,13 @@
|
||||
"when": 1774183202832,
|
||||
"tag": "0005_outgoing_lake",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "7",
|
||||
"when": 1774183919969,
|
||||
"tag": "0006_tiny_lockjaw",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -37,11 +37,13 @@ export const credentials = pgTable("harness_credentials", {
|
||||
|
||||
export const modelUsage = pgTable("harness_model_usage", {
|
||||
id: serial("id").primaryKey(),
|
||||
source: text("source").notNull().default("task"), // "task" | "chat"
|
||||
modelId: text("model_id").notNull(),
|
||||
provider: text("provider").notNull(),
|
||||
taskId: text("task_id").notNull(),
|
||||
taskSlug: text("task_slug").notNull(),
|
||||
iteration: integer("iteration").notNull(),
|
||||
taskId: text("task_id"),
|
||||
taskSlug: text("task_slug"),
|
||||
iteration: integer("iteration"),
|
||||
conversationId: text("conversation_id"),
|
||||
inputTokens: integer("input_tokens").notNull(),
|
||||
outputTokens: integer("output_tokens").notNull(),
|
||||
durationMs: integer("duration_ms").notNull(),
|
||||
|
||||
Reference in New Issue
Block a user