The Chat assistant¶
The second view of a project in the portal is a chat assistant that answers questions about your repo by calling WhyGraph's own tools. Ask "why does the rationale cache key on qualified name instead of node id?" or "who has touched the scan crawler in the last year?" and it goes and looks - reading rationale cards, walking evidence, querying the graph, running aggregate SQL, and drawing charts from the results.
Every tool it calls is the same in-process function the MCP server or the Explorer already calls. There is no second implementation and no MCP roundtrip, so the assistant cannot drift from what your editor sees.
It costs money and it writes to your database
Unlike the Explorer, the assistant is not read-only. Every turn calls an LLM under your chat provider, and every message, tool call, and session is stored in the project's WhyGraph database. Each model round is recorded, with its tokens and an estimated cost, on the portal's Usage & cost page (counts and cost only, never the chat itself). The portal is loopback-only with no login in local mode (see Localhost only).
If an organization, project or member budget has the hard stop on and is used up, the composer is replaced by a notice and a turn already running ends with "This chat has reached its monthly budget. You can still read everything that's already generated." Earlier conversations stay readable. The stop lifts when the budget is raised or the month (UTC) rolls over.
Getting started¶
Start the portal (whygraph up), open a project, and press + next to Chats in the sidebar
(or press g then c, or pick New chat in the command menu). You need a key for at least one of
the providers below, entered under Settings in the portal.
Pick a provider and model above the composer, type a question (or pick one of the starter questions on the empty page), and send. The first thing to know is that answers are grounded: when the assistant makes a claim it shows the tool calls behind it, and you can expand any of them to see what came back.
Symbols in an answer are links. The assistant writes them as whygraph://symbol/<qualified_name>,
and clicking one jumps to that symbol in the Explorer - graph recentered, detail panel open.
Providers¶
Chat needs streaming tool calls, which not every adapter supports. Four of WhyGraph's five LLM providers can drive it:
| Provider | Chat | Key |
|---|---|---|
anthropic |
Yes | Anthropic API key |
openai |
Yes | OpenAI API key |
deepseek |
Yes | DeepSeek API key |
openrouter |
Yes | OpenRouter API key |
ollama |
No | - |
ollama is excluded because local models' tool-calling reliability varies too much to depend on. It
still works for [analyze] and [rationale]. See
LLM providers.
Chat needs a key stored in Settings; keys in your shell do not count. With no key for any chat provider, New chat is disabled and the Chat page says what to do. A new chat starts on the provider set in Settings > Models and keys > Chat when it has a key, otherwise on the first provider that has one, and the picker lists only providers with a key. An older chat whose provider has lost its key still shows that provider, greyed out, with a note under the message box. Sending there is refused until you switch to a provider that has a key. The model pickers are also disabled while a monthly budget is hard-stopped.
The chat header shows what the session has cost so far ("This chat: ~$0.84") when you are allowed to see usage, and each reply ends with its token counts ("31,500 in · 2,250 out").
The model list may fall back
The picker asks your provider for its live model list. If your API key is scoped narrowly it can
work for chat but return 401 on the models endpoint - in that case WhyGraph shows a short
built-in list instead. That is normal operation, not an error.
Choosing a default¶
Adding a project doesn't ask about chat - you pick a provider and model per session in the composer.
The [chat] provider and model (the chat override in the project's Models settings) set the
defaults for new sessions, and nothing more: each session records the pair it was started with, so
changing your config never rewrites an existing conversation's history or re-answers it with a different
model.
[chat]
# provider = "anthropic" # default: the provider of [llm].model; without a key for it, the first provider that has one
# model = "claude-opus-4-7" # default: [llm].model, then the provider's default
Leave [chat] out and new sessions follow [llm].model, the default for every role. If that names a
provider that cannot chat (ollama), chat falls back to the first provider with a stored key; naming
it in [chat] itself is refused.
If OpenRouter is your provider, pin a tool-capable model rather than leaving openrouter/auto -
automatic routing can land on a model that does not support tool calling.
Effort¶
The Effort select sits beside the model picker above the composer. It sets how hard the model
thinks, for this chat, from the next reply on. The levels are low, medium, high, xhigh and
max.
Default uses the level set for chats under Settings > Models and keys > Chat, or the provider's own default when none is set.
Each provider receives the level in its own parameter:
| Provider | Sent as |
|---|---|
anthropic |
output_config.effort |
openai |
reasoning_effort |
deepseek |
reasoning_effort, passed through as it is |
openrouter |
reasoning.effort |
Some models have no effort setting: Claude Haiku 4.5, GPT-4o and GPT-4o mini. With a level chosen, a reply from one of them fails with an error saying the model does not take an effort level. Set Effort to Default for that chat.
On Anthropic, changing the effort starts a fresh prompt cache for that reply, so the first reply after a change costs a little more.
Modes¶
The Mode select beside the model pickers sets how the assistant approaches a question. A mode is a preset, not a restriction: every mode can use all twenty-three tools, and a mode only changes which ones the assistant reaches for first and how it words the answer. Each mode also has its own starter questions for an empty chat.
| Mode | For |
|---|---|
| Engineer | Code-first answers: symbols, files and lines, and why the code is the way it is. |
| Lead | The state of the work: what shipped, what is in flight or stuck, and who knows each area. |
You can switch a chat's mode at any time; it applies from the next reply, and each reply records the mode it was written in. Changing it starts a fresh prompt cache for that reply, as changing the effort does.
Which mode a new chat starts in. Your last-used mode in this project; if you have no chat yet, the
project's [chat].mode (Settings > Models and keys > Chat > Mode; see
the configuration reference); otherwise Engineer. There is no organization-wide
default. Modes are built into WhyGraph: a repository cannot add one or change one. A chat or a
[chat].mode that names a mode WhyGraph no longer has (early Onboarding and Product modes were
removed) runs as the project's default, else Engineer.
References in your question¶
When you name something WhyGraph can look up - a symbol, a file path, #412, a commit SHA - it
resolves the name against the index and the project database before the model sees the question, and
passes the result along with it: which names are real, where they live, whether a pull request or issue
is open, and whether a rationale card for a symbol is already cached. A name that does not resolve is
marked as such. The block carries identifiers and states only, never a pull request title or a commit
message. It is not shown in the chat, it is kept with your message so later replies in the chat see the
same text, and Continue and Regenerate reuse it rather than looking again. The lookup has a
1.5 second budget, reads from the index only (no git, no scan, no model call), and never fails a turn:
when it cannot finish, the question goes through as you wrote it.
WhyGraph never forces a tool call
The assistant is encouraged to look things up (by the mode, and by the references above), but a turn is never forced to call a tool. Some providers reject a forced tool choice when the model is thinking, and where one accepts it, it turns the thinking off for that reply. Forcing would trade answer quality for a guarantee, so WhyGraph measures instead: tool calls are counted by name in the project's agent activity.
Sessions¶
Your conversations are listed in the sidebar's Chats section, below All projects, newest activity first, each with how long ago it was last used. The 20 most recent show first; Show more adds the next 20. On a phone the list is in the navigation sheet, and tapping a chat closes the sheet.
- + opens a new chat. Nothing is saved until you send the first message: the session is created then, so opening a new chat and leaving never leaves an empty "New chat" behind.
- A new session is auto-titled from your first message; the title shows in the list while the first answer is still streaming, and a chat whose answer is streaming shows a small spinner.
- The ... menu on each chat has Rename (edit the title in place; Enter saves, Escape cancels) and Delete (asks first; it removes the chat's messages for good).
- Sessions are deep-linkable at
/p/<slug>/chat/<id>- copy the URL to come back to a conversation. - Sessions are stored in the project's WhyGraph database alongside the evidence, so they survive restarts and are per-repo, not global. In production each person sees only their own chats.
The Chats section appears only where you can chat: not for the Viewer role, not for a linked project (its chat lives on the platform), and not for a project that is not set up yet. When a hard-stopped budget is used up, + is disabled with the reason, and earlier chats stay readable.
Editing and regenerating¶
Hover a question and choose Edit to change it and get a new answer. Hover any reply and
choose Regenerate for another answer. Both keep the earlier version: a ‹ 2/3 › arrow beside the
message switches between the versions. A new version uses the chat's current provider, model and
effort.
Branch from here (on any question or reply) starts a new chat with the conversation up to and including that message, named after the original with "(branch)", and opens it. The original chat stays exactly as it was. Nothing is sent to the model, so it works even without a key; the new chat waits for your next message (or Regenerate, if you branched from a question).
Copy works on any message and copies its text. For a reply, that is the answer text, never its reasoning.
The actions and arrows are hidden while a reply is being written.
Rating a reply¶
A finished reply has a thumbs up and a thumbs down beside Regenerate. Thumbs down opens a small panel: pick what went wrong (Wrong, Incomplete, Not grounded, Too slow or Other) and, if you like, add a note of up to 500 characters. Click the active thumb again to clear the rating. The rating shows again when you reload the chat, and a Branch from here copy starts unrated.
Only the reply that ends a turn can be rated: not your question, and not a step in the middle of a turn that is still calling tools. A reply that ends in an error, a stop or the budget limit can be rated. Rating needs no key, works while another reply is being written and does not move the chat in the session list. Ratings and notes stay in the project's own database; they are never in a log, the usage ledger or the audit log, and only the person who started the chat reads them back.
For the developer of a local WhyGraph, scripts/chat_signals.py ranks the turns of a project's database
by these ratings and by what happened to them (regenerated, edited, continued, stopped, errored,
tool errors). It opens the database read-only and refuses a database with more than one chat owner
unless --owner is given. Its output holds your questions: do not commit it until each question is
rewritten.
Stop¶
Choose Stop while a reply is being written to end it at once. The text written so far stays in the chat, marked "Stopped." A tool call that is already running finishes first, so Stop can wait for that tool.
If you send right after stopping, the chat shows "Stopping the previous reply…" and sends your message as soon as the stopped reply has ended. A chat runs one reply at a time.
What it can look at¶
Twenty-three tools across six sources. You never call these directly; they are listed so you know what the assistant can and cannot reach.
-
Code structure (CodeGraph)
search_symbols,get_symbol,get_area_outline -
Evidence and rationale (WhyGraph)
get_rationale,get_evidence,get_area_history,find_changes,get_commit,get_pr,get_issue,get_repo_overview,list_recent_activity,get_work_in_flight,get_area_owners,compare_refs,get_milestones -
Statistics (aggregate SQL)
run_project_statsover commit history,run_graph_statsover the CodeGraph index -
Charts
render_chart -
Source tree
read_file,list_dir -
Team notes
find_notes,propose_note
Team notes¶
Project notes are short facts the team confirmed: decisions, constraints, gotchas, conventions. Two tools reach them:
find_notesreads confirmed notes only, by path, area, symbol, a text query, orproject_wide, at most ten per call. A note whose code changed since it was confirmed says so, and the assistant tells you.get_rationalecarries the notes that apply to its target too.propose_noteproposes one when you state a decision or ask the assistant to remember something - never on its own from commit messages, pull request or issue text - and at most twice in a turn (a third is declined, not an error).
A confirmed project purpose is a project-wide note: the assistant reads it
with find_notes like any other, and it counts in the overview's team_notes number. The overview
itself never carries its text. A proposed purpose is never read, and a linked project has none.
The assistant cites a note as note #12. Note text is data about the project: it never changes how
the assistant behaves. The assistant's starting overview carries only a count of project-wide
notes; it fetches their text with find_notes when your question is about conventions or constraints.
A proposal appears in the chat as a note card, built from the saved note rather than from the assistant's words. It shows the full text, the kind and the anchor, says that the assistant wrote it and that everyone who can open the project can see it, and offers Confirm (which confirms exactly what the card shows) and Discard. A project-wide proposal, or one on a top-level folder, shows Review instead, which opens the Notes page, because a wide note needs another member to confirm it. If an identical proposal already exists, the card says so and links to it. Reload the chat and the card shows the note's current status. The project's Overview counts what is waiting for review.
Unlike a chat, which is private to you, a note - proposals included - is visible to everyone who can open the project.
Areas, open work and owners¶
If the project configures areas, the assistant sees them and
can narrow find_changes with area (or a path, not both) and since_days;
list_recent_activity also takes since_days.
get_work_in_flightlists what is open right now: pull requests (drafts included) and issues, each with its idle time and a stale flag (a PR idle 7 days, an issue 30, by default; drafts are never stale), plus counts. Each PR also carries its review decision, who was asked to review (users and bots), when it got its first review, its assignees, its milestone and its diff size; each issue its assignees and milestone. Withareait keeps items placed in that area: a PR by its labels, else by its changed files, else by its commits when they are already scanned (area_sourcesays which); an issue by its labels only. It statesforge_data_as_of(when GitHub was last crawled) and lists anything it cannot know undernot_tracked, such as review requests to a team, deployments and incidents, so the assistant says "not tracked" instead of guessing. Items past the limit are counted inomitted.-
get_area_ownersanswers "who knows this code": for an area or a path (exactly one), each person's share of the commits, files touched, last touch and last activity anywhere, plus the CHAOSS Contributor Absence Factor (the fewest people whose shares reach half the commits) overall and among people active in the last 90 days. It counts default-branch commits in the window (365 days by default) and leaves out merges, bots and formatting or refactor sweeps;Co-authored-bytrailers and CODEOWNERS are not read. This is ownership of the code, not anyone's performance. -
compare_refsanswers "what lies between two fixed points": the commits one ref adds on top of another (from..to), the pull requests they belong to, the issues those closed, and a grouping by area. A ref is a tag name, a commit SHA (7 to 40 hex characters) or an ISO date (YYYY-MM-DD, the last default-branch commit on or before that day); leave the newer one out for the newest scanned commit. An unknown ref's error lists the newest tags. Use it for "what shipped in v1.4". get_milestonesanswers "how is this milestone tracking": due date, days to due, an overdue flag, GitHub's progress percentage and the open and closed counts of issues and pull requests, plus a burndown (open and closed items per day) for one milestone, which the assistant draws withrender_chart. Items count from their creation date; when they joined the milestone is not tracked.
Area matching follows the paths you configured; directory renames are not followed (see Areas).
Statistics are aggregate-only¶
The two *_stats tools let the assistant write its own SQL, which is why they are fenced hard. Every
query runs through a SQLite authorizer that permits read-only aggregates and nothing else, over an
explicit table allowlist. Results cap at 200 rows with a 5-second deadline.
The allowlist deliberately excludes chat_* (the assistant's own transcripts) and rationale_cache -
the assistant cannot query its own conversations, and coverage is reported through the proper tools
rather than by reading the cache table.
Reading files¶
read_file returns at most 400 lines or 100 KB per call, list_dir at most 200 entries, and
both refuse anything under .git/, .whygraph/, or .codegraph/, plus whygraph.toml itself - it may hold real API keys. WhyGraph's own storage is reachable only through the knowledge tools,
which present it in a curated shape instead of as raw database rows.
Charts¶
Ask for a chart and the assistant draws one from a statistics result - "chart commits per month for the last two years", "break down changes by author as a stacked bar".
Five kinds are available: line, bar, bar_h, bar_stacked, and bar_h_stacked. Stacked charts
carry at most six series.
Charts are drawn from data the assistant has already fetched, never from numbers it wrote itself.
A statistics tool mints an opaque reference for its result set, and render_chart can only point at
one of those references - so a chart is always a view onto real rows. References expire at the end of
the turn, and none is minted at all for an errored, truncated, or trivially short (fewer than two
rows) result, which is why the assistant will sometimes tell you there is nothing worth plotting.
Every chart ships with a Table toggle showing the underlying rows, and a PNG export.
Bounds on a turn¶
Tuned under [chat] - imported from a repository's whygraph.toml when you add the project:
| Key | Default | What it bounds |
|---|---|---|
max_tool_rounds |
8 |
Tool rounds per user turn. On the last round the assistant is called once more with tool calls turned off, so a turn always ends in prose rather than mid-investigation. |
max_rationale_generations |
2 |
Uncached rationale cards one turn may generate. 0 makes the assistant cache-only. |
context_token_budget |
60000 |
Conversation history sent to the model. |
stream_timeout_sec |
300 |
How long (30-1800 seconds) the provider may stay silent mid-reply - a long think or a large prompt - before the turn fails. |
max_tokens |
unset | Output one model call may write, 2048-128000 tokens. Unset means 32000 on Anthropic and the provider's own default elsewhere. |
max_continuations |
1 |
Automatic continuations (0-3) of a reply cut off by the output limit. |
Two behaviours worth knowing:
- Repeated calls are flagged. From the third identical tool call in one turn (same tool, same arguments) the call still runs, and its result tells the model how many times it asked.
- Lookups run side by side. When a step asks for several plain lookups at once (symbols, files, commits, pull requests), they run in parallel and the cards fill in in the model's order. Rationale cards, evidence, area history and statistics still run one at a time.
- A failing tool never ends a turn. Errors come back to the model as results, so it can correct course and try something else. Individual results are truncated at 30 000 characters with an explicit marker.
- Rationale generated inside chat uses your
[rationale]provider, not the chat session's. That keeps the cache key identical to what the Explorer and the MCP tools produce, so a card generated from a conversation is the same card your editor gets.
When history outgrows context_token_budget, WhyGraph first elides stale tool results, then drops
whole turns from the top - never splitting a tool call from its result.
How a turn ends¶
Most turns end with a reply. These cases are handled for you:
- A reply cut off by the output limit is continued, up to
max_continuationstimes. If it is still cut off, it ends with "The reply hit the output limit." and, on the latest reply, a Continue button that carries on from where it stopped. The rest is added to the same reply. - If the model writes nothing after using tools, WhyGraph asks it once to answer from what it gathered. This includes the last allowed round: when that round comes back empty (even if the model tried to call another tool, which is dropped), it is asked once more to answer in text.
- In a long turn, the bodies of older tool results are shortened to fit
context_token_budget, and the reply says "Older tool results in this turn were shortened to fit the context." - When the provider declines to answer, the reply says "The provider declined to answer."
- A reply that ends without any text says "The model ended its turn without a reply." and offers Regenerate.
The keys max_tokens and max_continuations come from a repository's whygraph.toml on import, or
from the API, the same way as max_tool_rounds. Only Effort has a field in Settings.
When a turn fails¶
A failed reply stays in the transcript with its reason, and the next question works normally: the model never sees an empty, failed or stopped reply again, nor a tool call whose result never arrived, so one bad turn cannot break the rest of the chat. A reply that ends without any text (after tool cards, or with nothing at all) says "The model ended its turn without a reply." instead of showing Thinking… forever. A reply cut off by the output limit or declined by the provider never runs a half-written tool call.
A dropped connection or a silence longer than stream_timeout_sec shows as the provider being
unreachable, and when the provider answered with an error the message ends with its request id
("request id: ..."), which you can quote to its support.
Reasoning models¶
DeepSeek and some OpenRouter models think before they answer and expect that reasoning sent back while they call tools. WhyGraph does that within a turn and, for DeepSeek, across turns too. A DeepSeek chat that has replies from before this was supported (or from another provider) runs without thinking until those replies leave the history the model sees.
What a model reasons shows in a collapsed Reasoning block above that step of its reply: open it to read along. The block stays open or closed the way you last left it (in this browser), survives a reload, and is never part of Copy. On Anthropic, WhyGraph asks for summarized thinking on every model that supports it; a model that does not is asked again without it, once. On a model where thinking is optional (Claude Opus 4.8, for example) this turns thinking on, which uses more output tokens per reply.
Limits¶
- One project per conversation - the project whose Chat you opened.
- No login and no multi-user support in local mode; the portal is for a single user on their own machine.
- The assistant cannot edit your code. It reads, queries, and charts.