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
2 changes: 1 addition & 1 deletion .claude/docs/shells.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Gotchas this shell paid for (all confirmed by `IntentBuilderShellIT`):
- **Never put an Alpine binding on `<i x-h-lucide>`.** The plugin REPLACES the `<i>` with the rendered svg, so a binding would be lost — it throws instead, and the uncaught throw **aborts Alpine's walk over everything below it** (the toolbar rendered, the whole split pane did not). Use an `<svg x-h-lucide>` placeholder for any `:data-lucide` / `:class` / `x-show`.
- **The idempotent creates answer `304`, and `response.ok` is 2xx-only.** `POST /workspaces/{ws}` and `.../{ws}/{project}` return 201 the first time and **304 NOT MODIFIED** afterwards, so a naive `!response.ok` throw makes the *second* save fail while the first succeeded. The fetch helper takes an `alsoOk: [304]`.
- **The workspace may not exist at all** on an instance where the user never opened the IDE; creating a project inside a missing workspace answers 404, so `ensureProject` creates the workspace first.
- The agent endpoint can make three upstream calls at 120s each — the client timeout is **7 minutes**, with a staged typing indicator.
- The agent endpoint can make three upstream calls, each **streamed** with adaptive thinking (so there is no fixed per-call ceiling to multiply; the server's outer bound is 10 minutes per call) — the client timeout is **20 minutes**, with a staged typing indicator.
- **Ask `GET /services/ide/intent/agent/status` whether the assistant is usable, never a throwaway `/agent` turn.** The shell probes on load so an unconfigured instance says so before the user types; the status endpoint reads configuration only, so it costs no upstream model call (a probe that posted a real turn would burn an Anthropic round-trip on every page load of a *working* instance).

The mxGraph rendering is **shared, not copied**: `editor-intent/js/intent-diagrams.js` exposes the framework-free `window.IntentDiagrams.render(model, host)` / `.dispose(host)`, extracted from the Intent Editor's controller and loaded by both (the shell by absolute URL). A standalone page must link **`platform-core/ui/styles/fonts.css`** or the glue cards' SAP-icon glyphs render as tofu. Tests: `IntentBuilderShellIT` — a `@Tag("smoke")` bootstrap check plus the full journey against a **local HTTP stub of the Anthropic upstream** (`DIRIGIBLE_INTENT_AI_BASE_URL`, the same override `IntentAgentServiceTest` uses in-process), so it needs no key and no network.
Expand Down
4 changes: 3 additions & 1 deletion components/engine/engine-intent/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ All implementations are Spring `@Component` beans implementing `IntentTargetGene

The third pane of the Intent Editor (toggled by the toolbar's discussion icon) is a natural-language assistant that edits `app.intent` at the developer's altitude - it **proposes a patch to the intent, never a re-emitted model file**, exactly the "edit shape, not file shape" contract from the design notes above.

- **Server-side bridge, key never leaves the server.** `agent/IntentAgentService` calls the Anthropic Messages API through the shared `ai/ModelClient`; `IntentAgentEndpoint` exposes `POST /services/ide/intent/agent` (body `{yaml, message, history}`, returns `{reply, proposedYaml}`) plus `GET /services/ide/intent/agent/status` → `{configured}` — the cheap "is the assistant usable at all?" probe (`IntentAgentService.isConfigured()`, configuration-only, **never** an upstream call) so a client can say the assistant is unavailable *before* the user's first message instead of after it fails with 412. The Builder shell probes it on load; do not implement that check by posting a throwaway turn, which would cost a real model round-trip on every page load. The whole agent feature is one cohesive `...intent.agent` package (DTO records + exceptions package-private). The API key is read via `DirigibleConfig.INTENT_AI_API_KEY` and is **never** sent to the browser. Config: `DIRIGIBLE_INTENT_AI_API_KEY` (blank → assistant disabled, endpoint returns `412`), `_MODEL` (default `claude-opus-4-8`), `_BASE_URL` (default `https://api.anthropic.com`), `_MAX_TOKENS` (`8192`), `_VERSION` (`2023-06-01`).
- **Server-side bridge, key never leaves the server.** `agent/IntentAgentService` calls the Anthropic Messages API through the shared `ai/ModelClient`; `IntentAgentEndpoint` exposes `POST /services/ide/intent/agent` (body `{yaml, message, history}`, returns `{reply, proposedYaml}`) plus `GET /services/ide/intent/agent/status` → `{configured}` — the cheap "is the assistant usable at all?" probe (`IntentAgentService.isConfigured()`, configuration-only, **never** an upstream call) so a client can say the assistant is unavailable *before* the user's first message instead of after it fails with 412. The Builder shell probes it on load; do not implement that check by posting a throwaway turn, which would cost a real model round-trip on every page load. The whole agent feature is one cohesive `...intent.agent` package (DTO records + exceptions package-private). The API key is read via `DirigibleConfig.INTENT_AI_API_KEY` and is **never** sent to the browser. Config: `DIRIGIBLE_INTENT_AI_API_KEY` (blank → assistant disabled, endpoint returns `412`), `_MODEL` (default `claude-opus-4-8`), `_BASE_URL` (default `https://api.anthropic.com`), `_MAX_TOKENS` (`32768`), `_VERSION` (`2023-06-01`).
- **The system prompt is an externalized, reviewable resource.** It lives in `engine-intent/src/main/resources/intent-assistant-guide.md` (loaded from the classpath by `ai/AssistantGuide.load(...)` at class init — fail-fast if missing), not an inline string, so it can be edited as documentation and kept in lockstep with what `IntentParser` enforces. It documents the full schema **including the declarative-glue catalog** (notifications/schedules/integrations/inbound/outbound/rollups) and the trigger `businessKey`/`businessKeyStrategy`, plus the recipient grammar (literal / direct field / one-hop `relation.field`, **no braces** — braces are only for `{…}` interpolation in `subject`/`body`).
- **Full file via a forced-available tool, then we diff.** The guide teaches Claude the intent YAML schema + the diff-stability rules (change minimally, preserve key order/comments, append don't reorder). Claude returns the **complete** updated YAML through a single `propose_intent` tool (`{explanation, yaml}`); plain-text replies (no tool call) are clarifying questions/answers. The editor renders the proposal as a Monaco **diff** against the current buffer; **Accept** replaces the buffer (Monaco `setValue` → the existing dirty-tracking + debounced re-parse fire), **Reject** discards. The developer still Saves + Generates as usual - **the agent never writes to disk or runs the generators.** This is why "full proposed YAML + we diff it" was chosen over LLM-authored unified diffs (fragile to apply) or structured edit-ops (lose comments/formatting).
- **Every proposal is validated server-side before it reaches the editor (bounded self-correction).** `IntentAgentService.chat` runs `IntentParser.parse` on the proposed YAML; on issues it replays the failed proposal as an assistant turn plus a corrective user turn (the parser's issue strings verbatim + "propose the corrected COMPLETE YAML") and calls the model again, at most `MAX_REPAIR_ROUNDS` (= 2) repair rounds per turn — so the first diff the developer sees is normally already valid, and a stubbornly invalid proposal cannot loop forever. After the last round the proposal is returned anyway with the outstanding issues appended to the reply text (the editor's inline validation still applies on Accept). An un-parseable proposal (YAML syntax error) counts as one issue. The extra upstream calls happen only when the first draft is invalid. `IntentAgentServiceTest` covers all four paths (valid first draft, plain-text reply, one repair round, exhausted rounds) with a scripted upstream — the exact first-user-session failure (`editable` listing a non-displayed field) is the fixture.
Expand All @@ -274,6 +274,8 @@ There are now **two** assistants, and everything they share lives in one package

`...intent.ai` is that shared layer: **`ModelClient`** (the only bridge to the Anthropic Messages API — the five `DIRIGIBLE_INTENT_AI_*` keys, the timeouts, the 412/502 error contract via `AssistantNotConfiguredException` / `AssistantUpstreamException`, the tool-call parsing, and the `messages(history, userTurn)` transcript builder that skips any non-`user`/`assistant` role), **`ProposalRepairLoop`** (the bounded validate-and-repair skeleton described above, parameterised by the tool-input member, the markdown fence, a validator and a repair prompt), **`AssistantGuide`** (fail-fast classpath load of a system prompt) and **`ChatTurn`** (the one transcript DTO both endpoints accept). **There is no second API key, no second HTTP client and no second guide loader** — a new assistant surface is a guide, a tool spec and a validator, nothing more.

**The call is streamed and thinks (#6955).** Three limits used to compound, and they tightened exactly as the authored application grew: one blocking request whose 120-second window had to contain the whole document; no `thinking` parameter at all, which on the configured default model means running with NO thinking (adaptive must be sent explicitly on the 4.7/4.8 family — the old `budget_tokens` form is rejected with a 400); and an 8192-token ceiling, against a tool contract that re-emits the COMPLETE `app.intent` every turn AND every repair round. So: `"stream": true` consumed as server-sent events (`assembleReply` concatenates the `text_delta`s as the answer and re-joins the matching `tool_use` block's `input_json_delta` fragments — matched by the block's **`index`**, never "the last tool_use seen", so a fragment of another block can never corrupt the proposal; thinking deltas, `ping`, and unknown event types are skipped, because the API's versioning policy says new ones may appear), `thinking: {"type": "adaptive"}` sent explicitly with the display default left alone (the reasoning is wanted for the answer's sake, not for anyone to read), and the `_MAX_TOKENS` default raised to 32768 — the env var is unchanged, only the fallback moved. Order mattered: streaming came first, because a larger budget or a longer reasoning pass without it converts a truncated answer into a timed-out one. The 412/502 contract is untouched, **including a mid-stream `error` event** (an overload the API reports inside a 200 response) mapping to the same `AssistantUpstreamException` a non-2xx status does; a proposal cut off mid-JSON says so and names the ceiling, and a `stop_reason: "max_tokens"` is logged, since neither cause is visible from either symptom. The per-request timeout is now an outer bound (10 min), not a window the answer must fit. The **default model is deliberately left alone** — with thinking sent explicitly, reasoning no longer depends on which model is configured. Both assistant surfaces go through this one client, so all three changes reached both. `ModelClientStreamTest` scripts a local SSE upstream (assembly, the request body, the interleaved-block case, mid-stream error, truncation, non-2xx); `IntentBuilderShellIT`'s stub streams its proposal in narrow fragments for the same reason.

`...intent.assist` is the **Workbench assistant** (issue #6724): the same integration standard, applied one altitude *down* — to the hand-written Java the intent boundary hands the developer (a `CalculatedField` action, a `JavaDelegate`, a custom controller). `POST /services/ide/intent/assist` (`{workspace, project, path, source, message, history}` → `{reply, proposedSource, diagnostics}`), `ADMINISTRATOR`/`DEVELOPER`, same 412/502.

- **Validation is a real compilation, and it must be batched.** `JavaAssistService` compiles a proposal with `JavaSourceCompiler.compileBatch` **together with every other Java source in the project** — `WorkspaceJavaSources` walks the workspace project for them — because that is exactly how the client-Java runtime compiles it: a `custom/` class exists to use the generated entities and repositories, so a proposal checked alone resolves none of its references and every round would "fail". `JavaAssistServiceTest` pins this by running the same proposal with and without its sibling. The compile is **side-effect-free**: never `JavaLoader.rebuild()` — that swaps the class loader and republishes the whole system.
Expand Down
Loading
Loading