Commit Graph
7 Commits
Author SHA1 Message Date
500806f001 fix(agent): the assistant reaches earlier räkenskapsår: period resolved from dates, years listed in the grounding (#2436)
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>
2026-09-08 21:32:38 +02:00
0bd3c27fba fix(assistant): the salary fact follows the ledger, never the column default (#2290)
* 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>
2026-09-05 09:49:21 +02:00
3edbf0a2e3 fix(agent): chat console keeps its thread across turns and reloads (#1859)
Three user-reported failures in the assistant panel, one root cause each:

1. "The chat asks what I'm referring to" when continuing a thread. The
   single-call console (general.help, AskConsole -> /api/agent/ask) was
   stateless since the 08-20 model-agnostic cutover: conversationId was only
   the tool actor id, so every turn was answered blind, reload or not.
   The provider-agnostic GenerateTextRequest gains an optional `history`
   (real message turns before the prompt, in both the Anthropic-family and
   the OpenAI-compatible adapter; absent/empty leaves the request
   byte-identical to the single-turn call). The route loads the thread's
   earlier turns server-side (loadChatHistory: text only, hidden and tool
   rows dropped, alternation repaired, newest 16 rows / 10k chars) before
   writing the new question, and hands them to the model.

2. A full page reload (the deploy prompt's "Ladda om") closed the docked
   panel and dropped the thread from view. The panel now remembers its open
   thread per tab in sessionStorage (lib/agent-panel/session-restore) and
   the provider reopens it on mount; the sheet loads it exactly like a pick
   from "Tidigare konversationer". Close and "Ny konversation" forget it; a
   thread that no longer opens is dropped instead of retried on every reload.

3. "Can't type any more" once the update banner shows. DeployReloadPrompt's
   full-width wrapper sits at z-[60] after the panel in DOM order and
   swallowed clicks on the panel's composer; only the card takes input now.


Claude-Session: https://claude.ai/code/session_01VjoXN3xdNZrHZeYA6qMi3g

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 16:37:30 +02:00
6aecbdc9b7 fix(agent): surface empty assistant answers instead of silent stops (#1830)
The support chat showed 'Tänker' and then went quiet with no answer and
no error. Root cause: the 2026-08-21 RIP-3 cutover moved general.help to
the single-call POST /api/agent/ask, whose 1500-token default cap made
stop_reason max_tokens routine on tool-loop turns. The empty answer then
passed unlogged through the service, the route answered 200 with an
empty string, and the console appended an invisible empty bubble.

Fixes, single-call path:
- ask-service: default maxTokens 1500 -> 5400 (the streaming chat's
  reply ceiling); an empty final answer now logs model + usage and
  throws the typed EmptyModelAnswerError instead of passing through.
- /api/agent/ask: maxDuration 300, logger, empty answer maps to 502
  with 'Assistenten gav inget svar. Försök igen.'; an empty assistant
  turn is never persisted (the question stays, so retry works).
- AskConsole: a 200 with an empty answer shows the error box instead of
  appending an invisible bubble.
- anthropic-family: serialized tool results are bounded at 40000 chars
  (mirrors run-turn) so one big read cannot eat the output budget; the
  step-exhausted fallback keeps tools declared with tool_choice none,
  because replaying tool_use/tool_result without tools is an API 400.

Fixes, streaming path (same silent class):
- /api/agent/invoke: maxDuration 300 so deep thinking turns are not
  killed mid-stream at the platform default cap.
- run-turn: stop_reason max_tokens with no visible text emits an error
  event, not a bare turn_complete.
- AgentChat: an NDJSON stream that ends without turn_complete or error
  (and was not aborted) shows 'Anslutningen bröts innan svaret blev
  klart. Försök igen.'

The lib/ai request-shape tests were updated deliberately for the
fallback change; general.help stays on the single-call runtime
(founder decision, not reverted).


Claude-Session: https://claude.ai/code/session_01SyDuePXxUFowaPBKpAv8SF

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 13:19:15 +02:00
ff4425d10e feat(agent): let the single-call assistant read the ledger via read-only MCP tools (#1767)
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>
2026-08-20 21:18:21 +02:00
3f6f1ab06e feat(chat): single-call console for general.help, persisted, runs on a local model (#1762)
RIP-3 cutover. The free-form /chat panel (general.help) now answers through a
page-scoped single-call console (AskConsole → POST /api/agent/ask) instead of
the streaming Anthropic runtime, so the in-app assistant runs on ANY configured
backend, including a local OpenAI-compatible model (Qwen behind
llama.cpp/Ollama/vLLM). No tool loop, no NDJSON stream, no Anthropic wire format.

Threads still persist: the ask route gains an opt-in persist branch that writes
both turns to agent_conversations/agent_messages as canonical Anthropic text
blocks, so the /chat sidebar and "resume a thread" keep working across old
streaming threads and new single-call ones. Page-scoped one-off asks (a report
page) omit persist and stay stateless.

Scope: only general.help is wired to the console. The tool-loop intents
(transaction.categorization, invoice.draft, supplier_invoice.review) and the
docked AgentSheet still use AgentChat + run-turn.ts because they stage
operations and need the tool loop, so run-turn.ts is intentionally NOT deleted
here (the plan gates its deletion on "once nothing calls them"; RIP-4 migrates
the rest).

- lib/agent/ask/persist.ts: resolveChatConversation (create/resume, ownership),
  persistUserTurn, persistAssistantTurn (append + roll last_message_* forward)
- app/api/agent/ask/route.ts: persist branch (resolve → user turn → answer →
  assistant turn), returns conversation_id; 404 on a foreign conversation
- components/agent/AskConsole.tsx: the console UI (approved sign-off design):
  user bubble + bare-prose answer, thinking indicator, empty/503/paywall states
- ChatConversationView / ChatNewStarter: branch general.help → AskConsole,
  every other intent keeps AgentChat unchanged

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-20 20:35:16 +02:00
a071a0220b feat(agent): provider-agnostic single-call assistant endpoint (runs on a local model) (#1759)
WS1 rip track, RIP-2 (audit Option A), stacked on the AI provider
abstraction (#1740). The first replacement for the streaming Anthropic chat
runtime: a page-scoped, single-call assistant answer.

- lib/agent/ask/ask-service.ts: answerAssistantQuestion() uses
  getAiService().generateText, so it runs on whatever backend is configured:
  AWS Bedrock, the direct Anthropic API, OR any OpenAI-compatible endpoint,
  including a local model (Qwen behind llama.cpp/Ollama/vLLM). No tool loop,
  no Anthropic wire format, nothing to translate per provider. The caller (a
  page) supplies the context; the service reads only the company's own
  profile for grounding, and the system prompt forbids inventing figures.
- POST /api/agent/ask: same auth/rate-limit/sandbox/paywall gates as
  /invoke, but gated on getAiStatus().configured (not assistantAvailable),
  because ANY provider works here. That is the difference that lets the
  assistant answer on a local model where the streaming /invoke returns 503.

This is the non-UI foundation of the rip: the thin /chat console and the
page-scoped actions (RIP-3, UI, gated on visual sign-off) will consume this
endpoint; run-turn.ts's streaming path and the intents' getAnthropic() usage
are removed once nothing calls them.

Verified: 10 unit tests (service prompt shape + tier + context-as-data +
truncation; route 401/429/400/403 paywall/200-on-openai-compatible/503
unconfigured) + a live smoke against a local OpenAI-compatible mock (resolved
provider openai-compatible, POSTed model qwen3.8 with a placeholder key,
returned an answer). 421 agent/ai tests green; tsc, guards, lint clean.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 19:48:43 +02:00