Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ Atelier helps teams ship the same context runtime everywhere: on the command lin

The runtime separates **Passive Tracking** (enabled by default) from **Active Context** (requires Development Mode).

![Atelier Overview dashboard](docs/assets/screenshots/overview.png)

## Passive Tracking (Production Ready)

- **Sessions & Ledger** — track every agent run and execution state
- **Expense Tracking** — aggregate token usage and estimated costs across all hosts
- **Traces** — record observable execution history (files, commands, errors)
- **Tools & Agents** — central registry of available capabilities and personas

![Cost & efficiency analytics across hosts, models, and tools](docs/assets/screenshots/analytics.png)

## Active Context (Development Mode)

_Enable with `ATELIER_DEV_MODE=1`_
Expand Down
Binary file added docs/assets/screenshots/analytics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/screenshots/external.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/screenshots/overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/screenshots/sessions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/screenshots/telemetry.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/screenshots/tools.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions frontend/src/pages/Traces.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,83 @@ describe("Traces page", () => {
expect(screen.getByText(/run-timeout/i)).toBeInTheDocument();
expect(screen.getByText(/Commands:/i)).toBeInTheDocument();
});

it("hides stale or unrelated highlighted snippets for the current search", async () => {
const user = userEvent.setup();
vi.spyOn(globalThis, "fetch").mockImplementation(
(input: RequestInfo | URL) => {
const url = String(input);

if (url.includes("/api/traces")) {
const params = new URL(url, "http://localhost").searchParams;
const query = params.get("query");
return Promise.resolve(
jsonResponse({
items: [
{
id: query ? "trace-sidecar" : "trace-base",
session_id: query ? "run-sidecar" : "run-base",
agent: "codex",
host: "codex",
domain: "coding",
task: query ? "Investigate sidecar session" : "Base session",
status: "success",
files_touched: [],
tools_called: [],
commands_run: [],
errors_seen: [],
repeated_failures: [],
validation_results: [],
created_at: "2026-05-12T00:00:00Z",
snippets: query
? [
"Tools: [[shopify]] sync service skills",
"Commands: inspect [[sidecar]] process logs",
]
: [],
},
],
metrics: {
stats: {
total: 1,
success: 1,
failed: 0,
partial: 0,
},
hosts: ["codex"],
domains: ["coding"],
},
})
);
}

return Promise.resolve(new Response("not found", { status: 404 }));
}
);

render(
<MemoryRouter initialEntries={["/sessions"]}>
<Routes>
<Route path="/sessions" element={<Traces />} />
</Routes>
</MemoryRouter>
);

await waitFor(() => {
expect(screen.getByText("Base session")).toBeInTheDocument();
});

await user.type(
screen.getByPlaceholderText(
/Search tasks, reasoning, tools, commands, files, validations, and summaries/i
),
"sidecar"
);

expect(
await screen.findByText("Investigate sidecar session")
).toBeInTheDocument();
expect(screen.getByText(/inspect/i)).toBeInTheDocument();
expect(screen.queryByText(/shopify/i)).not.toBeInTheDocument();
});
});
55 changes: 51 additions & 4 deletions frontend/src/pages/Traces.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState, useMemo } from "react";
import { useEffect, useState, useMemo, useRef } from "react";
import { useSearchParams } from "react-router-dom";
import {
api,
Expand Down Expand Up @@ -27,6 +27,7 @@ export default function Traces() {
const [query, setQuery] = useState<string>(initialQuery);
const [searchInput, setSearchInput] = useState<string>(initialQuery);
const [page, setPage] = useState(0);
const tracesRequestSeq = useRef(0);

useEffect(() => {
const urlQuery = searchParams.get("q") ?? "";
Expand Down Expand Up @@ -70,21 +71,28 @@ export default function Traces() {

// Fetch traces when filters change
useEffect(() => {
const requestSeq = ++tracesRequestSeq.current;
let cancelled = false;
setLoading(true);
setPage(0);
setErr(null);
api
.traces(50, 0, domainFilter, hostFilter, query)
.then((res) => {
if (cancelled || requestSeq !== tracesRequestSeq.current) return;
setItems(res.items);
setMetrics(res.metrics);
setHasMore(res.items.length >= 50);
setLoading(false);
})
.catch((e) => {
if (cancelled || requestSeq !== tracesRequestSeq.current) return;
setErr(String(e));
setLoading(false);
});
return () => {
cancelled = true;
};
}, [domainFilter, hostFilter, query]);

useEffect(() => {
Expand Down Expand Up @@ -126,18 +134,21 @@ export default function Traces() {

const loadMore = () => {
if (loading || !hasMore) return;
const requestSeq = tracesRequestSeq.current;
setLoading(true);
const nextOffset = (page + 1) * 50;
api
.traces(50, nextOffset, domainFilter, hostFilter, query)
.then((res) => {
if (requestSeq !== tracesRequestSeq.current) return;
setItems((prev) => (prev ? [...prev, ...res.items] : res.items));
setMetrics(res.metrics);
setHasMore(res.items.length >= 50);
setPage((p) => p + 1);
setLoading(false);
})
.catch((e) => {
if (requestSeq !== tracesRequestSeq.current) return;
setErr(String(e));
setLoading(false);
});
Expand Down Expand Up @@ -296,6 +307,7 @@ export default function Traces() {
<TraceCard
key={t.id}
trace={t}
searchQuery={query}
isExpanded={expandedId === t.id}
onToggle={() => toggleExpanded(t.id)}
onOpenInspector={() => openInspector(t)}
Expand Down Expand Up @@ -337,11 +349,13 @@ export default function Traces() {

function TraceCard({
trace,
searchQuery,
isExpanded,
onToggle,
onOpenInspector,
}: {
trace: Trace;
searchQuery: string;
isExpanded: boolean;
onToggle: () => void;
onOpenInspector: () => void;
Expand Down Expand Up @@ -393,7 +407,7 @@ function TraceCard({
{trace.task}
</p>
{trace.snippets && trace.snippets.length > 0 && (
<TraceSearchHits snippets={trace.snippets} />
<TraceSearchHits snippets={trace.snippets} query={searchQuery} />
)}
<div className="flex items-center gap-3 text-[10px] text-neutral-500 font-mono">
<span>Session: {trace.session_id}</span>
Expand All @@ -415,10 +429,21 @@ function TraceCard({
);
}

function TraceSearchHits({ snippets }: { snippets: string[] }) {
function TraceSearchHits({
snippets,
query,
}: {
snippets: string[];
query: string;
}) {
const matchingSnippets = snippets.filter((snippet) =>
snippetMatchesSearchQuery(snippet, query)
);
if (matchingSnippets.length === 0) return null;

return (
<div className="mb-2 mt-2 space-y-1.5">
{snippets.slice(0, 4).map((snippet, index) => (
{matchingSnippets.slice(0, 4).map((snippet, index) => (
<div
key={`${snippet}-${index}`}
className="border border-amber-900/30 bg-amber-950/10 px-2.5 py-1.5 text-[11px] leading-relaxed text-neutral-300"
Expand All @@ -430,6 +455,28 @@ function TraceSearchHits({ snippets }: { snippets: string[] }) {
);
}

function searchTerms(query: string): string[] {
const terms = Array.from(query.matchAll(/"([^"]+)"|(\S+)/g))
.map((match) => (match[1] || match[2] || "").trim().toLowerCase())
.flatMap((term) => term.split(/[^0-9a-z_]+/i))
.filter(Boolean);
return [...new Set(terms)];
}

function snippetMatchesSearchQuery(snippet: string, query: string): boolean {
const terms = searchTerms(query);
if (terms.length === 0) return true;

const markedTerms = Array.from(snippet.matchAll(/\[\[(.*?)\]\]/g))
.map((match) => match[1].trim().toLowerCase())
.flatMap((term) => term.split(/[^0-9a-z_]+/i))
.filter(Boolean);

return markedTerms.some((marked) =>
terms.some((term) => marked.startsWith(term) || term.startsWith(marked))
);
}

function SnippetText({ text }: { text: string }) {
const compact = text.replace(/\s+/g, " ").trim();
const parts = compact.split(/(\[\[.*?\]\])/g);
Expand Down
55 changes: 32 additions & 23 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -335,37 +335,46 @@ main() {
fi

if [[ "$ATELIER_NO_SERVICECTL" != "1" ]]; then
info "Starting Atelier background service controller..."
if [[ "$ATELIER_DRY_RUN" == "1" ]]; then
echo "[dry-run] $ATELIER_BIN_DIR/atelier servicectl start --interval-seconds $ATELIER_SERVICECTL_INTERVAL_SECONDS --maintenance-interval-seconds $ATELIER_SERVICECTL_MAINTENANCE_INTERVAL_SECONDS"
else
"$ATELIER_BIN_DIR/atelier" servicectl start \
--interval-seconds "$ATELIER_SERVICECTL_INTERVAL_SECONDS" \
--maintenance-interval-seconds "$ATELIER_SERVICECTL_MAINTENANCE_INTERVAL_SECONDS" >/dev/null
fi
else
info "Skipping background service controller because ATELIER_NO_SERVICECTL=1"
fi
if command -v systemctl >/dev/null 2>&1 || [[ "$(uname -s)" == "Darwin" ]]; then
info "Registering Atelier services with background manager..."
local background_args=()
if [[ "$ATELIER_NO_STACK" != "1" && $(command -v docker) ]]; then
background_args+=("--with-stack")
fi

STACK_STARTED=0
if [[ "$ATELIER_NO_STACK" != "1" ]]; then
if command -v docker >/dev/null 2>&1; then
info "Starting Atelier visualization stack (service + frontend)..."
if [[ "$ATELIER_DRY_RUN" == "1" ]]; then
echo "[dry-run] $ATELIER_BIN_DIR/atelier stack start"
echo "[dry-run] $ATELIER_BIN_DIR/atelier background install ${background_args[*]}"
else
"$ATELIER_BIN_DIR/atelier" stack start \
&& STACK_STARTED=1 \
|| warn "Visualization stack did not start (Docker daemon may not be running)"
"$ATELIER_BIN_DIR/atelier" background install "${background_args[@]}" >/dev/null
fi
else
info "Skipping visualization stack because Docker is not installed"
info "Starting Atelier background service controller (loose process)..."
if [[ "$ATELIER_DRY_RUN" == "1" ]]; then
echo "[dry-run] $ATELIER_BIN_DIR/atelier servicectl start --interval-seconds $ATELIER_SERVICECTL_INTERVAL_SECONDS --maintenance-interval-seconds $ATELIER_SERVICECTL_MAINTENANCE_INTERVAL_SECONDS"
else
"$ATELIER_BIN_DIR/atelier" servicectl start \
--interval-seconds "$ATELIER_SERVICECTL_INTERVAL_SECONDS" \
--maintenance-interval-seconds "$ATELIER_SERVICECTL_MAINTENANCE_INTERVAL_SECONDS" >/dev/null
fi

if [[ "$ATELIER_NO_STACK" != "1" ]]; then
if command -v docker >/dev/null 2>&1; then
info "Starting Atelier visualization stack (service + frontend)..."
if [[ "$ATELIER_DRY_RUN" == "1" ]]; then
echo "[dry-run] $ATELIER_BIN_DIR/atelier stack start"
else
"$ATELIER_BIN_DIR/atelier" stack start \
&& STACK_STARTED=1 \
|| warn "Visualization stack did not start (Docker daemon may not be running)"
fi
fi
fi
fi
else
info "Skipping visualization stack because ATELIER_NO_STACK=1"
info "Skipping background services because ATELIER_NO_SERVICECTL=1"
fi

if [[ "$STACK_STARTED" == "1" ]]; then
if [[ "$STACK_STARTED" == "1" || ( "$ATELIER_NO_SERVICECTL" != "1" && $(command -v systemctl) && "$ATELIER_NO_STACK" != "1" ) ]]; then
echo " Visualization stack is running:"
echo " frontend: http://localhost:3125"
echo " service: http://localhost:8787"
Expand All @@ -374,7 +383,7 @@ main() {
echo " Commands:"
echo " atelier --version - Check core CLI version"
echo " atelier-mcp --version - Check MCP server version"
echo " atelier servicectl status - View background service and systemctl status"
echo " atelier background status - View background service status"
echo " atelier stack start - Start production API and Frontend (requires Docker)"
echo " atelier stack stop - Stop the visualization stack"
echo " atelier stack logs - View stack logs"
Expand Down
8 changes: 4 additions & 4 deletions src/atelier/core/capabilities/archival_recall/capability.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __init__(self, store: MemoryStore, embedder: Embedder, *, redactor: Callable
def archive(
self,
*,
agent_id: str,
agent_id: str | None = None,
text: str,
source: ArchivalSource,
source_ref: str = "",
Expand All @@ -40,7 +40,7 @@ def archive(
for idx, chunk in enumerate(chunks):
embedding = embeddings[idx] if idx < len(embeddings) and embeddings[idx] else None
passage = ArchivalPassage(
agent_id=agent_id,
agent_id=agent_id or "shared",
text=chunk,
embedding=embedding,
embedding_model=self._embedder.name if embedding is not None else "",
Expand All @@ -60,7 +60,7 @@ def archive(
def recall(
self,
*,
agent_id: str,
agent_id: str | None,
query: str,
top_k: int = 5,
tags: list[str] | None = None,
Expand Down Expand Up @@ -98,7 +98,7 @@ def recall(
recall_query = widened_query
selected = [item.passage for item in ranked]
recall = MemoryRecall(
agent_id=agent_id,
agent_id=agent_id or "shared",
query=recall_query,
top_passages=[passage.id for passage in selected],
selected_passage_id=selected[0].id if selected else None,
Expand Down
8 changes: 6 additions & 2 deletions src/atelier/core/foundation/retriever.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,16 @@ def count_reasonblock_tokens(block: ReasonBlock) -> int:
return count_tokens(render_block_for_agent(block))


def passage_in_agent_scope(passage: ArchivalPassage, requested_agent_id: str) -> bool:
def passage_in_agent_scope(passage: ArchivalPassage, requested_agent_id: str | None) -> bool:
"""Return whether a passage may be injected for the requested agent."""
if requested_agent_id is None:
return True
return passage.agent_id == requested_agent_id or "agent:any" in passage.tags


def filter_scoped_passages(passages: Sequence[ArchivalPassage], *, requested_agent_id: str) -> list[ArchivalPassage]:
def filter_scoped_passages(
passages: Sequence[ArchivalPassage], *, requested_agent_id: str | None
) -> list[ArchivalPassage]:
"""Keep only same-agent passages and explicit global lessons."""
return [passage for passage in passages if passage_in_agent_scope(passage, requested_agent_id)]

Expand Down
Loading
Loading