diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa6a62..5230e15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ new version heading in the same commit. ## [Unreleased] +## [0.222.0] — 2026-07-16 +### Added +- **Chat renders KB pages and hosted apps inline too.** Extends the inline-deliverable cards (v0.220.0) + beyond Library artifacts: when an agent writes a Knowledge Base page (`kb_write`) or builds/changes a + hosted app (`app_create`/`app_update`) mid-conversation, the Chat window now shows a titled tile for it + — the KB tile deep-links to `#/kb/
/`, the app tile to `#/apps/` plus an **Open** link + to the live app when it's published. Unlike artifact ids (minted server-side, parsed from the tool + result), the KB `section`/`slug` and app `id` come straight from the tool **input**, captured + optimistically and dropped if the write comes back an error. The `/api/sessions/:id/conversation` route + resolves them into viewer-safe `ChatKbRef`/`ChatAppRef` cards (KB pages and apps are tenant-wide surfaces + every member can already browse; an unknown/deleted ref is dropped). Also gave those tools clearer + activity labels ("Updated the knowledge base", "Built an app", "Updated an app"). + ## [0.221.0] — 2026-07-16 ### Added - **Session activity trail — every object a run opened, with live status.** The per-session activity diff --git a/package-lock.json b/package-lock.json index a8d2508..f02653d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-os", - "version": "0.221.0", + "version": "0.222.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-os", - "version": "0.221.0", + "version": "0.222.0", "license": "MIT", "bin": { "agent-os": "bin/agent-os" diff --git a/package.json b/package.json index 6cefbb8..840e988 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-os", - "version": "0.221.0", + "version": "0.222.0", "description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.", "license": "MIT", "type": "commonjs", diff --git a/src/edge/conversation.ts b/src/edge/conversation.ts index a1f6737..368e132 100644 --- a/src/edge/conversation.ts +++ b/src/edge/conversation.ts @@ -30,6 +30,25 @@ export interface ChatArtifactRef { raw: string; } +/** A viewer-safe reference to a Knowledge Base page an activity wrote (`kb_write`). The chat UI renders a + * titled tile deep-linking to `#/kb/
/`. Resolved by the /conversation route from the + * activity's `kbRefs`; a page the KB no longer has is dropped. */ +export interface ChatKbRef { + section: string; + slug: string; + title: string; +} + +/** A viewer-safe reference to a hosted App an activity built or changed (`app_create` / `app_update`). The + * chat UI renders a tile deep-linking to `#/apps/` (and, when published, an "open" link to the live + * app). Resolved by the /conversation route from the activity's `appIds`; an unknown id is dropped. */ +export interface ChatAppRef { + id: string; + name: string; + icon?: string; + published: boolean; +} + /** One entry in the human-readable timeline. */ export type ChatTurn = | { kind: 'user'; text: string; ts: number } @@ -46,9 +65,15 @@ export type ChatTurn = * parsed from the tool result. The /conversation route resolves these to rich {@link * ChatArtifactRef} cards the chat UI renders inline; unresolvable ids are dropped there. */ artifactIds?: string[]; - /** Resolved, viewer-filtered artifact previews (populated by the /conversation route from - * {@link ChatArtifactRef}); absent in the raw transcript parse. */ + /** KB page(s) this activity wrote (`kb_write`), captured from the tool INPUT (section/slug). */ + kbRefs?: { section: string; slug: string }[]; + /** Hosted app id(s) this activity built or changed (`app_create` / `app_update`), from the INPUT. */ + appIds?: string[]; + /** Resolved, viewer-filtered previews (populated by the /conversation route from {@link + * ChatArtifactRef}/{@link ChatKbRef}/{@link ChatAppRef}); absent in the raw transcript parse. */ artifacts?: ChatArtifactRef[]; + kbPages?: ChatKbRef[]; + apps?: ChatAppRef[]; ts: number; }; @@ -140,8 +165,11 @@ function friendlyTool(name: string, input: Record): { label: st if (/^slack_/.test(bare)) return { label: 'Sent a Slack message', detail: clip(input.text ?? input.message ?? input.channel) }; if (/^discord_/.test(bare)) return { label: 'Sent a Discord message', detail: clip(input.content ?? input.message) }; if (/^(remember|recall|revise|forget)$/.test(bare)) return { label: 'Used its memory' }; + if (bare === 'kb_write') return { label: 'Updated the knowledge base', detail: clip(input.title ?? input.slug) }; if (/^kb_/.test(bare)) return { label: 'Used the knowledge base', detail: clip(input.query ?? input.slug ?? input.title) }; if (/^task_/.test(bare)) return { label: 'Updated the task board', detail: clip(input.title) }; + if (bare === 'app_create') return { label: 'Built an app', detail: clip(input.name ?? input.id) }; + if (bare === 'app_update') return { label: 'Updated an app', detail: clip(input.id) }; // Artifact-producing tools: keep a distinct label so the inline artifact card reads naturally under it. if (bare === 'publish') return { label: 'Published to the Library', detail: clip(input.title ?? basename(input.path)) }; if (bare === 'image_generate') return { label: 'Created an image', detail: clip(input.prompt) }; @@ -157,6 +185,22 @@ function friendlyTool(name: string, input: Record): { label: st /** Bare (mcp-stripped) names of tools whose result yields Library artifact id(s) we render inline. */ const ARTIFACT_TOOLS = new Set(['publish', 'image_generate', 'image_edit', 'video_generate']); +/** From a tool_use's bare name + INPUT, derive the KB / app reference(s) that call SUCCEEDS produces — the + * keys (section/slug, app id) live in the input, so unlike artifact ids we needn't parse the result text. + * Attached to the activity card at tool_use time and cleared if the tool_result comes back an error. */ +function refsFromInput(bare: string, input: Record): { kbRefs?: { section: string; slug: string }[]; appIds?: string[] } { + if (bare === 'kb_write') { + const section = typeof input.section === 'string' ? input.section.trim() : ''; + const slug = typeof input.slug === 'string' ? input.slug.trim() : ''; + if (section && slug) return { kbRefs: [{ section, slug }] }; + } + if (bare === 'app_create' || bare === 'app_update') { + const id = typeof input.id === 'string' ? input.id.trim().toLowerCase() : ''; + if (id) return { appIds: [id] }; + } + return {}; +} + /** Flatten a tool_result's `content` (string, or an array of `{type:'text',text}` blocks) to plain text. */ function resultText(content: unknown): string { if (typeof content === 'string') return content; @@ -221,18 +265,27 @@ export function readConversation(claudeSessionId: string): Conversation { const text = String(b.text || '').trim(); if (text) turns.push({ kind: o.type === 'user' ? 'user' : 'assistant', text, ts }); } else if (b.type === 'tool_use') { - const { label, detail } = friendlyTool(String(b.name || ''), (b.input || {}) as Record); + const rawName = String(b.name || ''); + const input = (b.input || {}) as Record; + const { label, detail } = friendlyTool(rawName, input); activityById.set(String(b.id), turns.length); - turns.push({ kind: 'activity', tool: String(b.name || ''), label, detail, status: 'running', ts }); + // KB / app refs come from the INPUT (the section/slug or app id) — attach them optimistically now; + // a failed tool_result clears them below so a broken write never shows a card. + const { kbRefs, appIds } = refsFromInput(bareName(rawName), input); + turns.push({ kind: 'activity', tool: rawName, label, detail, status: 'running', kbRefs, appIds, ts }); } else if (b.type === 'tool_result') { const idx = activityById.get(String(b.tool_use_id)); if (idx != null) { const card = turns[idx]; if (card && card.kind === 'activity') { card.status = b.is_error ? 'error' : 'ok'; - // Artifact-producing tool that succeeded → capture the Library id(s) it returned, so the - // route can resolve them into inline preview cards. - if (!b.is_error && ARTIFACT_TOOLS.has(bareName(card.tool))) { + if (b.is_error) { + // The write failed — drop the optimistic KB/app refs so no card is shown. + delete card.kbRefs; + delete card.appIds; + } else if (ARTIFACT_TOOLS.has(bareName(card.tool))) { + // Artifact-producing tool that succeeded → capture the Library id(s) it returned, so the + // route can resolve them into inline preview cards. const ids = extractArtifactIds(resultText(b.content)); if (ids.length) card.artifactIds = ids; } diff --git a/src/server.ts b/src/server.ts index cd350f5..18542f9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -17,7 +17,7 @@ import { exampleCapabilities } from './capabilities/examples'; import { evaluate } from './observability/evaluation'; import { TerminalManager, AGENT_OS_OPERATING_NOTES } from './terminal'; import { classifyActivity, clipText, ActivityCategory, ActivityEffect, ActivityTarget } from './state/session-activity'; -import { readConversation, type ChatArtifactRef } from './edge/conversation'; +import { readConversation, type ChatArtifactRef, type ChatKbRef, type ChatAppRef } from './edge/conversation'; import { summarizeConversation } from './edge/summarize'; import { Automation, Automations, nextCronRun, derivedConcurrencyCap, chatTitle } from './edge/automations'; import { SlackSocket } from './edge/slack-socket'; @@ -2237,28 +2237,47 @@ async function handle(os: AgentOS, tm: TerminalManager, autos: Automations, req: if (!tm.canViewSession(id, me)) return sendJson(res, 403, { error: 'not allowed to view this session' }); const claudeId = tm.sessionClaudeId(id); const convo = claudeId ? readConversation(claudeId) : { turns: [], found: false }; - // Resolve any artifact ids an activity produced (publish / image_generate / …) into viewer-safe - // preview cards the chat UI renders inline — dropping ids the viewer may not see or that no longer - // exist. The viewer already passed canViewSession, so their own run's deliverables resolve. + // Resolve the deliverables an activity produced (a Library artifact, a KB page, a hosted app) into + // viewer-safe preview cards the chat UI renders inline — dropping anything the viewer may not see or + // that no longer exists. The viewer already passed canViewSession, so their own run's outputs resolve; + // KB pages and apps are tenant-wide surfaces every member can already browse. for (const t of convo.turns) { - if (t.kind !== 'activity' || !t.artifactIds?.length) continue; - const refs: ChatArtifactRef[] = []; - for (const aid of t.artifactIds) { - const a = os.artifacts.get(aid); - if (!a) continue; - if (!tm.canViewSpawn(a.source ?? null, me) && !a.sharedTeam) continue; - refs.push({ - id: a.id, - title: a.title || a.filename, - kind: a.kind, - mime: a.mime, - filename: a.filename, - isImage: a.mime.startsWith('image/'), - isVideo: a.mime.startsWith('video/'), - raw: `/api/artifacts/${a.id}/raw`, - }); + if (t.kind !== 'activity') continue; + if (t.artifactIds?.length) { + const refs: ChatArtifactRef[] = []; + for (const aid of t.artifactIds) { + const a = os.artifacts.get(aid); + if (!a) continue; + if (!tm.canViewSpawn(a.source ?? null, me) && !a.sharedTeam) continue; + refs.push({ + id: a.id, + title: a.title || a.filename, + kind: a.kind, + mime: a.mime, + filename: a.filename, + isImage: a.mime.startsWith('image/'), + isVideo: a.mime.startsWith('video/'), + raw: `/api/artifacts/${a.id}/raw`, + }); + } + if (refs.length) t.artifacts = refs; + } + if (t.kbRefs?.length) { + const refs: ChatKbRef[] = []; + for (const r of t.kbRefs) { + const page = os.kb.read(os.tenant, r.section, r.slug); + if (page) refs.push({ section: page.section, slug: page.slug, title: page.title }); + } + if (refs.length) t.kbPages = refs; + } + if (t.appIds?.length) { + const refs: ChatAppRef[] = []; + for (const aid of t.appIds) { + const app = os.apps.get(aid); + if (app) refs.push({ id: app.id, name: app.name, icon: app.icon, published: !!app.published }); + } + if (refs.length) t.apps = refs; } - if (refs.length) t.artifacts = refs; } return sendJson(res, 200, { agent, ...convo }); } diff --git a/web/src/App.tsx b/web/src/App.tsx index 1cd27cb..0ce1888 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -12,7 +12,7 @@ import { Input } from '@/components/ui/input' import { Separator } from '@/components/ui/separator' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdown-menu' -import { api, EFFORTS, PERMISSION_MODES, type PermissionMode, type StateResp, type AgentInfo, type Session, type Msg, type Member, type Role, type TeamResp, type MemberIdentity, type IdentityProvider, IDENTITY_PROVIDERS, type Automation, type Task, type TaskEvent, type TaskAttachment, type TaskStatus, type AddTaskReq, type Goal, type GoalEvent, type GoalStatus, type GoalCounts, type GoalProgress, type AddGoalReq, type MemoryRecord, type MemoryHealth, type MemoryBackend, type MemorySettings, type MemorySettingsReq, type OllamaStatus, type KbPage, type KbRevision, type AgentRevision, type AgentStats, type Recommendation, type DigestConfig, type DigestModel, type DreamingState, type Measurement, type Insights, type ImprovementTile, type MemoryCleanupPlan, type KbTidyPlan, type StuckGoal, type TroubledAutomation, type PolicyDocument, type PolicyRule, type PolicyOutcome, type PolicyOp, type PolicyProposal, type PolicyRevision, type DirListing, type FileEntry, type FileContent, type Artifact, type AppInfo, type AppFile, type AppCapabilities, type SkillSummary, type SkillsResp, type CatalogSkill, type CatalogAgent, type SkillSource, type RemoteSkill, type SkillshHit, type SkillRequest, type SecretRequest, type IntegrationsResp, type SlackStatus, type DiscordStatus, type AuditEvent, type Effort, type RuntimeTuning, type Concurrency, type SecretMeta, type UpdateStatus, type UpdateApplyResult, type ActivityEvent, type ActivitySummaryRow, type SystemMetrics, type DepsReport, type DepStatus, type DepsInstallResult, type ChatTurn, type ChatArtifactRef } from '@/lib/api' +import { api, EFFORTS, PERMISSION_MODES, type PermissionMode, type StateResp, type AgentInfo, type Session, type Msg, type Member, type Role, type TeamResp, type MemberIdentity, type IdentityProvider, IDENTITY_PROVIDERS, type Automation, type Task, type TaskEvent, type TaskAttachment, type TaskStatus, type AddTaskReq, type Goal, type GoalEvent, type GoalStatus, type GoalCounts, type GoalProgress, type AddGoalReq, type MemoryRecord, type MemoryHealth, type MemoryBackend, type MemorySettings, type MemorySettingsReq, type OllamaStatus, type KbPage, type KbRevision, type AgentRevision, type AgentStats, type Recommendation, type DigestConfig, type DigestModel, type DreamingState, type Measurement, type Insights, type ImprovementTile, type MemoryCleanupPlan, type KbTidyPlan, type StuckGoal, type TroubledAutomation, type PolicyDocument, type PolicyRule, type PolicyOutcome, type PolicyOp, type PolicyProposal, type PolicyRevision, type DirListing, type FileEntry, type FileContent, type Artifact, type AppInfo, type AppFile, type AppCapabilities, type SkillSummary, type SkillsResp, type CatalogSkill, type CatalogAgent, type SkillSource, type RemoteSkill, type SkillshHit, type SkillRequest, type SecretRequest, type IntegrationsResp, type SlackStatus, type DiscordStatus, type AuditEvent, type Effort, type RuntimeTuning, type Concurrency, type SecretMeta, type UpdateStatus, type UpdateApplyResult, type ActivityEvent, type ActivitySummaryRow, type SystemMetrics, type DepsReport, type DepStatus, type DepsInstallResult, type ChatTurn, type ChatArtifactRef, type ChatKbRef, type ChatAppRef } from '@/lib/api' import { type Branding, type PublicBranding, type NotificationPrefs, DEFAULT_NOTIFICATION_PREFS, type PromptShortcut } from '@/lib/api' import { applyAccent, applyFavicon, faviconDataUri, readableOn } from '@/lib/branding' import { ConnectorsPage, GithubMineCard } from '@/connectors' @@ -3061,11 +3061,43 @@ function ArtifactCard({ a }: { a: ChatArtifactRef }) { ) } +/** A KB page an agent wrote, as an inline tile deep-linking to `#/kb/
/`. */ +function KbCard({ p }: { p: ChatKbRef }) { + return ( + + + + {p.title} + {p.section}/{p.slug} + + + + ) +} + +/** A hosted app an agent built or changed — a tile into the Apps console, plus an "Open" link to the live + * app when it's published (a proposed app isn't routable yet, so it only deep-links for review). */ +function AppCard({ a }: { a: ChatAppRef }) { + return ( + + ) +} + /** One friendly activity card ("Sent a Slack message" ✓). Terminal detail stays hidden by default. An - * artifact-producing activity (publish / image_generate / …) shows its deliverable(s) inline beneath. */ + * activity that produced a deliverable (a Library artifact, a KB page, a hosted app) shows it inline beneath. */ function ActivityCard({ turn }: { turn: Extract }) { const dot = turn.status === 'error' ? 'bg-destructive' : turn.status === 'ok' ? 'bg-emerald-500' : 'bg-amber-400 animate-pulse' + const hasCards = !!(turn.artifacts?.length || turn.kbPages?.length || turn.apps?.length) return (
@@ -3073,9 +3105,11 @@ function ActivityCard({ turn }: { turn: Extract {turn.label} {turn.detail && {turn.detail}}
- {turn.artifacts && turn.artifacts.length > 0 && ( + {hasCards && (
- {turn.artifacts.map((a) => )} + {turn.artifacts?.map((a) => )} + {turn.kbPages?.map((p) => )} + {turn.apps?.map((a) => )}
)}
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index a12d98b..ec94c20 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -1061,11 +1061,26 @@ export interface ChatArtifactRef { raw: string } +/** A viewer-safe KB page preview attached to a chat activity (`kb_write`). Deep-links to #/kb/
/. */ +export interface ChatKbRef { + section: string + slug: string + title: string +} + +/** A viewer-safe hosted-app preview attached to a chat activity (`app_create`/`app_update`). */ +export interface ChatAppRef { + id: string + name: string + icon?: string + published: boolean +} + /** One entry in the non-technical chat timeline (mirrors src/edge/conversation.ts). */ export type ChatTurn = | { kind: 'user'; text: string; ts: number } | { kind: 'assistant'; text: string; ts: number } - | { kind: 'activity'; tool: string; label: string; detail?: string; status: 'running' | 'ok' | 'error'; artifactIds?: string[]; artifacts?: ChatArtifactRef[]; ts: number } + | { kind: 'activity'; tool: string; label: string; detail?: string; status: 'running' | 'ok' | 'error'; artifactIds?: string[]; artifacts?: ChatArtifactRef[]; kbPages?: ChatKbRef[]; apps?: ChatAppRef[]; ts: number } export interface ConversationResp { agent?: string turns: ChatTurn[]