summaryrefslogtreecommitdiff
path: root/Omni/Jr/Web.hs
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-11-30 22:03:54 -0500
committerBen Sima <ben@bensima.com>2025-11-30 22:03:54 -0500
commit725b98000aed836ac5808b3afbda4ce869956156 (patch)
treeb81d63d3a76261dc73457c88560bb8cc4fa3f13c /Omni/Jr/Web.hs
parent9fa7697cd979eaa15a2479819463c3bdd86cc99a (diff)
Extract facts from completed tasks after review acceptance
Perfect! Let me verify the complete implementation checklist against the ✅ **1. In Jr.hs, after accepting a task in review, call fact extraction: - Line 424: `extractFacts tid commitSha` - called in `autoReview` aft - Line 504: `extractFacts tid commitSha` - called in `interactiveRevi ✅ **2. Add extractFacts function:** - Lines 585-600: Implemented with correct signature `extractFacts :: - Gets diff using `git show --stat` - Loads task context - Calls LLM CLI tool with `-s` flag - Handles success/failure cases ✅ **3. Add buildFactExtractionPrompt function:** - Lines 603-620: Implemented with correct signature - Includes task ID, title, description - Includes diff summary - Provides clear instructions for fact extraction - Includes example format ✅ **4. Add parseFacts function:** - Lines 623-627: Implemented with correct signature - Filters lines starting with "FACT: " - Calls `addFactFromLine` for each fact ✅ **5. Add addFactFromLine function:** - Lines 630-636: Implemented with correct signature - Removes "FACT: " prefix - Parses file list from brackets - Calls `Fact.createFact` with project="Omni", confidence=0.7, source - Prints confirmation message ✅ **6. Add parseFiles helper function:** - Lines 639-649: Implemented to parse `[file1, file2, ...]` format ✅ **7. Import for Omni.Fact module:** - Line 22: `import qualified Omni.Fact as Fact` already present ✅ **8. Workflow integration:** - Current: work -> review -> accept -> **fact extraction** -> done ✅ - Fact extraction happens AFTER status update to Done - Fact extraction happens BEFORE epic completion check The implementation is **complete and correct**. All functionality descri 1. ✅ Facts are extracted after task review acceptance (both auto and man 2. ✅ LLM is called with proper context (task info + diff) 3. ✅ Facts are parsed and stored with correct metadata (source_task, con 4. ✅ All tests pass (`bild --test Omni/Agent.hs`) 5. ✅ No linting errors (`lint Omni/Jr.hs`) The feature is ready for use and testing. When a task is completed and a 1. The LLM will be prompted to extract facts 2. Any facts learned will be added to the knowledge base 3. Each fact will have `source_task` set to the task ID 4. Facts can be viewed with `jr facts list` Task-Id: t-185
Diffstat (limited to 'Omni/Jr/Web.hs')
-rw-r--r--Omni/Jr/Web.hs184
1 files changed, 181 insertions, 3 deletions
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index fe1711b..86647d4 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -241,6 +241,7 @@ type API =
:> QueryParam "sort" Text
:> Get '[Lucid.HTML] TaskListPartial
:<|> "partials" :> "task" :> Capture "id" Text :> "metrics" :> Get '[Lucid.HTML] TaskMetricsPartial
+ :<|> "partials" :> "task" :> Capture "id" Text :> "events" :> QueryParam "since" Int :> Get '[Lucid.HTML] AgentEventsPartial
data CSS
@@ -261,7 +262,7 @@ data InterventionPage = InterventionPage TaskCore.HumanActionItems SortOrder UTC
data TaskListPage = TaskListPage [TaskCore.Task] TaskFilters SortOrder UTCTime
data TaskDetailPage
- = TaskDetailFound TaskCore.Task [TaskCore.Task] [TaskCore.TaskActivity] (Maybe TaskCore.RetryContext) [GitCommit] (Maybe TaskCore.AggregatedMetrics) UTCTime
+ = TaskDetailFound TaskCore.Task [TaskCore.Task] [TaskCore.TaskActivity] (Maybe TaskCore.RetryContext) [GitCommit] (Maybe TaskCore.AggregatedMetrics) [TaskCore.StoredEvent] UTCTime
| TaskDetailNotFound Text
data GitCommit = GitCommit
@@ -330,6 +331,8 @@ newtype TaskListPartial = TaskListPartial [TaskCore.Task]
data TaskMetricsPartial = TaskMetricsPartial Text [TaskCore.TaskActivity] (Maybe TaskCore.RetryContext) UTCTime
+data AgentEventsPartial = AgentEventsPartial [TaskCore.StoredEvent] Bool UTCTime
+
data DescriptionViewPartial = DescriptionViewPartial Text Text Bool
data DescriptionEditPartial = DescriptionEditPartial Text Text Bool
@@ -1487,7 +1490,7 @@ instance Lucid.ToHtml TaskDetailPage where
"The task "
Lucid.code_ (Lucid.toHtml tid)
" could not be found."
- toHtml (TaskDetailFound task allTasks activities maybeRetry commits maybeAggMetrics now) =
+ toHtml (TaskDetailFound task allTasks activities maybeRetry commits maybeAggMetrics agentEvents now) =
let crumbs = taskBreadcrumbs allTasks task
in Lucid.doctypehtml_ <| do
pageHead (TaskCore.taskId task <> " - Jr")
@@ -1588,6 +1591,8 @@ instance Lucid.ToHtml TaskDetailPage where
Lucid.class_ "review-link-btn"
]
"Review This Task"
+
+ renderAgentLogSection (TaskCore.taskId task) agentEvents (TaskCore.taskStatus task) now
where
renderDependency :: (Monad m) => TaskCore.Dependency -> Lucid.HtmlT m ()
renderDependency dep =
@@ -2386,6 +2391,162 @@ renderInlinePart part = case part of
InlineCode txt -> Lucid.code_ [Lucid.class_ "md-inline-code"] (Lucid.toHtml txt)
BoldText txt -> Lucid.strong_ (Lucid.toHtml txt)
+renderAgentLogSection :: (Monad m) => Text -> [TaskCore.StoredEvent] -> TaskCore.Status -> UTCTime -> Lucid.HtmlT m ()
+renderAgentLogSection tid events status now = do
+ let shouldShow = not (null events) || status == TaskCore.InProgress
+ when shouldShow <| do
+ let isInProgress = status == TaskCore.InProgress
+ pollAttrs =
+ if isInProgress
+ then
+ [ Lucid.makeAttribute "hx-get" ("/partials/task/" <> tid <> "/events"),
+ Lucid.makeAttribute "hx-trigger" "every 3s",
+ Lucid.makeAttribute "hx-swap" "innerHTML"
+ ]
+ else []
+ Lucid.div_ ([Lucid.class_ "agent-log-section", Lucid.id_ "agent-log-container"] <> pollAttrs) <| do
+ Lucid.h3_ <| do
+ Lucid.toHtml ("Agent Log (" <> tshow (length events) <> ")")
+ when isInProgress <| Lucid.span_ [Lucid.class_ "agent-log-live"] " LIVE"
+ if null events
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "Agent session starting..."
+ else do
+ Lucid.div_ [Lucid.class_ "agent-log"] <| do
+ traverse_ (renderAgentEvent now) events
+ agentLogScrollScript
+
+renderAgentEvent :: (Monad m) => UTCTime -> TaskCore.StoredEvent -> Lucid.HtmlT m ()
+renderAgentEvent now event =
+ let eventType = TaskCore.storedEventType event
+ content = TaskCore.storedEventContent event
+ timestamp = TaskCore.storedEventTimestamp event
+ eventId = TaskCore.storedEventId event
+ in Lucid.div_
+ [ Lucid.class_ ("agent-event agent-event-" <> eventType),
+ Lucid.makeAttribute "data-event-id" (tshow eventId)
+ ]
+ <| do
+ case eventType of
+ "Assistant" -> renderAssistantEvent content timestamp now
+ "ToolCall" -> renderToolCallEvent content timestamp now
+ "ToolResult" -> renderToolResultEvent content timestamp now
+ "Cost" -> renderCostEvent content
+ "Error" -> renderErrorEvent content timestamp now
+ "Complete" -> renderCompleteEvent timestamp now
+ _ -> Lucid.div_ [Lucid.class_ "event-unknown"] (Lucid.toHtml content)
+
+renderAssistantEvent :: (Monad m) => Text -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderAssistantEvent content timestamp now =
+ Lucid.div_ [Lucid.class_ "event-assistant"] <| do
+ Lucid.div_ [Lucid.class_ "event-header"] <| do
+ Lucid.span_ [Lucid.class_ "event-icon"] "💬"
+ Lucid.span_ [Lucid.class_ "event-label"] "Assistant"
+ renderRelativeTimestamp now timestamp
+ Lucid.div_ [Lucid.class_ "event-content event-bubble"] <| do
+ let truncated = Text.take 2000 content
+ isTruncated = Text.length content > 2000
+ Lucid.toHtml truncated
+ when isTruncated <| Lucid.span_ [Lucid.class_ "event-truncated"] "..."
+
+renderToolCallEvent :: (Monad m) => Text -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderToolCallEvent content timestamp now =
+ let (toolName, args) = parseToolCallContent content
+ in Lucid.details_ [Lucid.class_ "event-tool-call"] <| do
+ Lucid.summary_ <| do
+ Lucid.span_ [Lucid.class_ "event-icon"] "🔧"
+ Lucid.span_ [Lucid.class_ "event-label tool-name"] (Lucid.toHtml toolName)
+ renderRelativeTimestamp now timestamp
+ Lucid.div_ [Lucid.class_ "event-content tool-args"] <| do
+ renderCollapsibleOutput args
+
+renderToolResultEvent :: (Monad m) => Text -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderToolResultEvent content timestamp now =
+ let lineCount = length (Text.lines content)
+ isLong = lineCount > 20
+ in Lucid.div_ [Lucid.class_ "event-tool-result"] <| do
+ Lucid.div_ [Lucid.class_ "event-header result-header"] <| do
+ Lucid.span_ [Lucid.class_ "event-icon"] "📋"
+ Lucid.span_ [Lucid.class_ "event-label"] "Result"
+ when (lineCount > 1)
+ <| Lucid.span_ [Lucid.class_ "line-count"] (Lucid.toHtml (tshow lineCount <> " lines"))
+ renderRelativeTimestamp now timestamp
+ if isLong
+ then
+ Lucid.details_ [Lucid.class_ "result-collapsible"] <| do
+ Lucid.summary_ "Show output"
+ Lucid.pre_ [Lucid.class_ "event-content tool-output"] (Lucid.toHtml content)
+ else Lucid.pre_ [Lucid.class_ "event-content tool-output"] (Lucid.toHtml content)
+
+renderCostEvent :: (Monad m) => Text -> Lucid.HtmlT m ()
+renderCostEvent content =
+ Lucid.div_ [Lucid.class_ "event-cost"] <| do
+ Lucid.span_ [Lucid.class_ "event-icon"] "💰"
+ Lucid.span_ [Lucid.class_ "cost-text"] (Lucid.toHtml content)
+
+renderErrorEvent :: (Monad m) => Text -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderErrorEvent content timestamp now =
+ Lucid.div_ [Lucid.class_ "event-error"] <| do
+ Lucid.div_ [Lucid.class_ "event-header"] <| do
+ Lucid.span_ [Lucid.class_ "event-icon"] "❌"
+ Lucid.span_ [Lucid.class_ "event-label"] "Error"
+ renderRelativeTimestamp now timestamp
+ Lucid.div_ [Lucid.class_ "event-content error-message"] (Lucid.toHtml content)
+
+renderCompleteEvent :: (Monad m) => UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderCompleteEvent timestamp now =
+ Lucid.div_ [Lucid.class_ "event-complete"] <| do
+ Lucid.span_ [Lucid.class_ "event-icon"] "✅"
+ Lucid.span_ [Lucid.class_ "event-label"] "Session completed"
+ renderRelativeTimestamp now timestamp
+
+parseToolCallContent :: Text -> (Text, Text)
+parseToolCallContent content =
+ case Text.breakOn ":" content of
+ (name, rest)
+ | Text.null rest -> (content, "")
+ | otherwise -> (Text.strip name, Text.strip (Text.drop 1 rest))
+
+renderCollapsibleOutput :: (Monad m) => Text -> Lucid.HtmlT m ()
+renderCollapsibleOutput content =
+ let lineCount = length (Text.lines content)
+ in if lineCount > 20
+ then
+ Lucid.details_ [Lucid.class_ "output-collapsible"] <| do
+ Lucid.summary_ (Lucid.toHtml (tshow lineCount <> " lines"))
+ Lucid.pre_ [Lucid.class_ "tool-output-pre"] (Lucid.toHtml content)
+ else Lucid.pre_ [Lucid.class_ "tool-output-pre"] (Lucid.toHtml content)
+
+agentLogScrollScript :: (Monad m) => Lucid.HtmlT m ()
+agentLogScrollScript =
+ Lucid.script_
+ [ Lucid.type_ "text/javascript"
+ ]
+ ( Text.unlines
+ [ "(function() {",
+ " var log = document.querySelector('.agent-log');",
+ " if (log) {",
+ " var isNearBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 100;",
+ " if (isNearBottom) {",
+ " log.scrollTop = log.scrollHeight;",
+ " }",
+ " }",
+ "})();"
+ ]
+ )
+
+instance Lucid.ToHtml AgentEventsPartial where
+ toHtmlRaw = Lucid.toHtml
+ toHtml (AgentEventsPartial events isInProgress now) = do
+ Lucid.h3_ <| do
+ Lucid.toHtml ("Agent Log (" <> tshow (length events) <> ")")
+ when isInProgress <| Lucid.span_ [Lucid.class_ "agent-log-live"] " LIVE"
+ if null events
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "Agent session starting..."
+ else do
+ Lucid.div_ [Lucid.class_ "agent-log"] <| do
+ traverse_ (renderAgentEvent now) events
+ agentLogScrollScript
+
api :: Proxy API
api = Proxy
@@ -2422,6 +2583,7 @@ server =
:<|> readyCountHandler
:<|> taskListPartialHandler
:<|> taskMetricsPartialHandler
+ :<|> agentEventsPartialHandler
where
styleHandler :: Servant.Handler LazyText.Text
styleHandler = pure Style.css
@@ -2584,7 +2746,8 @@ server =
if TaskCore.taskType task == TaskCore.Epic
then Just </ liftIO (TaskCore.getAggregatedMetrics tid)
else pure Nothing
- pure (TaskDetailFound task tasks activities retryCtx commits aggMetrics now)
+ agentEvents <- liftIO (TaskCore.getEventsForTask tid)
+ pure (TaskDetailFound task tasks activities retryCtx commits aggMetrics agentEvents now)
taskStatusHandler :: Text -> StatusForm -> Servant.Handler StatusBadgePartial
taskStatusHandler tid (StatusForm newStatus) = do
@@ -2725,6 +2888,21 @@ server =
maybeRetry <- liftIO (TaskCore.getRetryContext tid)
pure (TaskMetricsPartial tid activities maybeRetry now)
+ agentEventsPartialHandler :: Text -> Maybe Int -> Servant.Handler AgentEventsPartial
+ agentEventsPartialHandler tid maybeSince = do
+ now <- liftIO getCurrentTime
+ maybeSession <- liftIO (TaskCore.getLatestSessionForTask tid)
+ events <- case maybeSession of
+ Nothing -> pure []
+ Just sid -> case maybeSince of
+ Nothing -> liftIO (TaskCore.getEventsForSession sid)
+ Just lastId -> liftIO (TaskCore.getEventsSince sid lastId)
+ tasks <- liftIO TaskCore.loadTasks
+ let isInProgress = case TaskCore.findTask tid tasks of
+ Nothing -> False
+ Just task -> TaskCore.taskStatus task == TaskCore.InProgress
+ pure (AgentEventsPartial events isInProgress now)
+
taskToUnixTs :: TaskCore.Task -> Int
taskToUnixTs t = round (utcTimeToPOSIXSeconds (TaskCore.taskUpdatedAt t))