Part 3 of #2185. A user with several imported years concluded the assistant
"only reads the period I am standing in". Nothing restricted it: the report
tools defaulted to the most recent fiscal period when no period_id was given
and then rejected any from_date/to_date/as_of_date outside it, and neither
the snapshot nor the chat identity block told the model which years existed
or how to address them.
- extensions/general/mcp-server/server.ts: resolveReportPeriod takes a date
hint; without period_id, a date in the call resolves the fiscal period that
contains it (findFiscalPeriodContaining, company-scoped), and a date no
period covers fails with the company's span instead of the latest year's
bounds. Income statement (from_date or to_date), balance sheet (as_of_date)
and dimension P&L (to_date) use it. The range guard is unchanged. No schema
change: tools/list sits at its token ceiling.
- lib/agent/fiscal-years.ts: one query and one line, "Räkenskapsår (senaste
först): ... period_id=<uuid> (senaste|avslutat)", plus the rule on
addressing an earlier year, shared by the single-call snapshot and the
streaming chat's always-on identity block.
- lib/agent/intents/shared-rules.ts and TOOL_RULES: pass that year's
period_id, one call per year, and say which räkenskapsår the answer covers.
Claude-Session: https://claude.ai/code/session_0179bdetHyofL6ATfQxB5wP5
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* fix(assistant): the salary fact follows the ledger, never the column default
The in-app assistant told a payroll-running aktiebolag in every answer that
it "betalar inte löner" (support case 2026-09-04). company_settings.
pays_salaries is NOT NULL DEFAULT false and only the Skatt settings form
writes it, so for every company that never opened that form the flag reads
false whatever the ledger says, and lib/agent/ask/snapshot.ts asserted that
default as a fact.
- Trigger salary_runs_booked_marks_employer (20260904191000): a booked
salary run sets pays_salaries = true and fills a never-attested
employer_registered, at the one place every writer (dashboard, MCP, v1,
seeders) passes through. Backfill for the 12 companies on prod already
booking payroll with the flag at its default (8 of them also lacked the
employer flag, and with it their AGI deadline reminders). An explicit
employer_registered = false stays the user's answer.
- The assistant snapshot applies the composer's employee-facts doctrine:
positive evidence (active employees, the flag, an attested employer
registration) yields the fact, only an attested negative yields the
negative, the default yields nothing. It also names Inställningar >
Skatt / > Bokföring so the model can point at the page.
- The composer's KÄNDA FAKTA no longer prints "Betalar ut lön: nej" from
the same default.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S62AGZwsMoBc8x8obBDVqE
* fix(migration): NOT EXISTS instead of NOT IN for the reset-source exclusion
A NULL source_company_id in the subquery would make NOT IN never true and
silently skip the whole backfill.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S62AGZwsMoBc8x8obBDVqE
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The /chat assistant (audit Option A / rip) shipped in #1759 reading only the
company name + entity type, so it answered "jag har ingen bokföringsdata" to
every figures question ("vad är min största utgiftspost?"). It now behaves like
an MCP client: it answers over a bounded, READ-only tool loop across the same
MCP read tools the old streaming assistant had, plus an always-on company
snapshot as the backstop.
Provider-agnostic by construction, so it still runs on a local model:
- lib/ai generateText gains optional `tools` + `maxSteps`. The OpenAI-compatible
service forwards them to the Vercel AI SDK (stopWhen: stepCountIs), which runs
the loop; the Anthropic-family service hand-rolls a small loop against
messages.create. Kept on the raw Anthropic SDK: no new deps, and the no-tools
path is byte-identical, so hosted extraction/composer/etc. are unchanged.
- lib/agent/ask/ledger-tools.ts: the read slice of general.help's whitelist
(income statement, VAT, ledgers, query_journal, reskontror, lists…) from
agentToolRegistry, dispatched with the agent_chat actor run-turn uses. Write/
staging + memory-write tools are excluded; readOnlyHint/destructiveHint are
re-checked. Empty in a core-only build → snapshot-only, graceful.
- lib/agent/ask/snapshot.ts: a compact company_settings + deadlines block so a
model that can't/won't call tools still answers status questions. Never carries
figures (those come from the live tools).
- ask-service attaches tools + snapshot when a userId is present and uses a
tool-aware system prompt; the route calls ensureInitialized() so the registry
is populated and threads userId/conversationId through.
Works on Bedrock and on any local model with function-calling (Qwen). Tests:
the anthropic hand-rolled loop (tool call → result → answer, is_error handling,
step-budget forced answer), openai tool forwarding, the read-only adapter
filter, the snapshot format, and the ask-service wiring. 457 agent+ai tests
green, lint/guards clean.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>