c608792aeae647d1032881aa0b71e96f890f8ae4
63 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3675f264f1 |
Infer record pickers for record-typed logic function workflow inputs (#21494)
## Context Logic functions can declare workflow inputs typed as records or arrays of records (e.g. the People Data Labs enrichment functions), but the workflow builder rendered those as a plain text input with a variable picker, which is not usable. ## What this does - Adds an `objectUniversalIdentifier` link on input schema properties, so a record-typed input is tied to a workspace object. - The SDK build infers it from a `TwentyRecord<'objectUniversalIdentifier'>` marker type in the handler signature, reading the object's universal identifier straight from the source; explicit input schemas can still set the field directly. - The workflow builder renders these inputs as a single record picker or a record multi-select with the variable picker on the right. Selected records are stored as record ids; `TwentyRecord<UID>` is a branded `string`, so the handler signature reflects that it receives ids (a bound variable resolves to whatever the referenced step produced). - The multi-select collapses overflowing chips into a `+N` badge (reusing `ExpandableList`) and its variable picker offers both record objects and fields. - Updates the People Data Labs enrichment inputs as the reference implementation. <img width="802" height="824" alt="CleanShot 2026-06-12 at 16 54 10@2x" src="https://github.com/user-attachments/assets/a0896d74-0aab-49bd-a173-14c578a2e533" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21494?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
ccc77932a0 |
Tool execution metrics (#21587)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21587?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
bb6da7b7d1 |
feat(code-interpreter): reuse a warm sandbox per conversation (E2B) (#21664)
## What
The E2B code-interpreter driver created a **fresh sandbox on every
execution** and killed it in `finally`, so every call in a conversation
paid full cold-start and started blank. This PR keeps **one warm sandbox
per conversation** and, on idle, **pauses** it rather than killing it.
## How
- **Discovery without a registry:** the sandbox is tagged with the chat
`threadId` (scoped `workspaceId:threadId`) via E2B **metadata**, found
with `Sandbox.list({ query: { state: ['running','paused'], metadata }
})` and resumed with `Sandbox.connect()` (which auto-resumes a paused
sandbox). E2B is the source of truth — no Redis/DB mapping.
- **Pause/resume (E2B 2.x):** session sandboxes are created with
`lifecycle: { onTimeout: 'pause', autoResume: true }`. When idle they
**pause** — compute billing stops, filesystem **and** kernel/memory
state are preserved — and resume in ~1s on the next call. This replaces
the earlier keepalive approach.
- **No premature pause mid-run:** the sandbox is kept alive for
`max(execution timeout, idle window)`, so a long execution is never
paused underneath itself.
- **Tenant isolation:** discovery filters by the `twentySessionId` tag
and **re-checks it client-side**, so a loose server-side match can never
hand one conversation's warm sandbox (with its files, kernel state,
token) to another.
- **Concurrency:** executions sharing a session are serialized
in-process (one active stream per thread, run as a single job — the chat
resolver queues concurrent messages), so parallel tool calls can't race
the shared kernel.
- **Output isolation:** `/home/user/output` is reset at the start of
each reused run, so a call only returns the artifacts it actually
produced; durable state lives elsewhere and persists.
## SDK upgrade
`@e2b/code-interpreter` **`^1.0.4` → `^2.6.0`** (pulls `e2b@2.x`). The
typed pause/resume API, `lifecycle`, and the `state`/`metadata` list
filter only exist in the 2.x line; 1.x exposed them only as untyped
OpenAPI internals. `Sandbox.list()` is now a paginator (handled).
## Config
| Var | Default | Purpose |
|---|---|---|
| `CODE_INTERPRETER_TIMEOUT_MS` | `300000` | Max single-execution
duration. |
| `CODE_INTERPRETER_IDLE_TIMEOUT_MS` | `300000` | Idle window before the
warm sandbox auto-pauses. |
Reuse is always-on when a session id is present (chat path). The
workflow-agent path and the dev-only `LocalDriver` are unaffected.
## ⚠️ Open item before merge: paused-sandbox GC
E2B retains paused sandboxes **indefinitely** (no TTL). Unlike the old
keepalive path (which auto-killed on idle), pause means a conversation's
sandbox persists after the chat ends — so without garbage collection,
paused sandboxes accumulate (≈ one per historical conversation) and
consume storage. A GC policy is required; the approach + retention
window are being decided (see PR discussion). Also: the E2B runtime path
can't run in CI, so this still needs a **live smoke test** (reuse hit,
idle→pause, resume) and confirmation of paused-storage pricing before
rollout.
## Tests / checks
- Resolver unit tests (`getOrCreateSessionSandbox`): reuse+extend,
create-when-absent, duplicate reaping, connect-failure fallback,
keep-first-connectable-when-earlier-dead, **ignore cross-tenant
metadata**, and **kill-on-timeout-refresh-failure**.
- `nx typecheck twenty-server` (against e2b 2.x), `oxlint --type-aware`,
`oxfmt --check` all clean.
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
1e8169ca3e |
feat(ai-agent): suggest similar tool names when tool discovery misses (#21654)
## Context When the in-product AI agent guesses a tool name that doesn't exist, the discovery tools dead-end it with no way to recover. The most common failure is a singular/plural slip — e.g. the agent tries `group_by_cloud_user` when the real tool is `group_by_cloud_users` (read/bulk tools are always plural; only `find_one_*` is singular). Today both `learn_tools` and `execute_tool` reply with a flat `Could not find: <name>` and no suggestion, so the agent burns turns guessing or gives up. ## Change - Add a `findSimilarToolNames` util that ranks catalog tool names against the missed name by Levenshtein distance (reusing the existing `getEditDistance`), with a small bonus for a shared `<operation>_` prefix so the correct same-operation plural is ranked first rather than a closer-but-different operation (e.g. `find_many_person` → `find_many_people`, not `find_one_person`). - `learn_tools`: when names aren't found, include `suggestions` in the structured result and inline them in the message — `Could not find: group_by_person (did you mean: group_by_people?).` - `execute_tool` (via `ToolRegistryService.resolveAndExecute`): append `Did you mean: …?` to the not-found error, reusing the catalog it already fetched (no extra lookup). The heuristic mirrors the existing workflow variable-path suggestion util (same edit-distance threshold), so behavior is consistent with that prior art. ## Tests - Unit tests for `findSimilarToolNames`: plural recovery, prefix-aware ranking, distance threshold, 3-suggestion cap, empty catalog. - `learn_tools` tool tests: suggestions surfaced on a miss; no suggestion lookup when all names resolve. `nx typecheck twenty-server` passes; `oxlint --type-aware` and `oxfmt --check` are clean on the changed files. https://claude.ai/code/session_01GMjZkJYkqTJogJJTM6AAV8 --- _Generated by [Claude Code](https://claude.ai/code/session_01GMjZkJYkqTJogJJTM6AAV8)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21654?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
1efa3567ef |
Rename isUIReadOnly to isUIEditable, add isUICreatable, expose both to app developers (#21504)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
# UI capability flags: `isUIEditable` + `isUICreatable`
## Per-verb capability model
This PR replaces the negative `isUIReadOnly` metadata flag with
positive, per-verb capability flags (à la Salesforce
`createable`/`updateable`):
- **`isUIEditable: boolean`, default `true`** — rename of `isUIReadOnly`
with inverted polarity, on **both** `objectMetadata` and
`fieldMetadata`. It is one concept ("can the user edit this through the
generic UI?") at two altitudes, so it carries one name at both levels.
- **`isUICreatable: boolean`, default `true`** — new, **object-level
only** (fields have no create verb). When `false`, no generic UI
affordance to create a record of this object appears anywhere (table "+"
buttons, board column add, calendar add, relation-section "Add new",
record picker "Add new", command-menu create action and its keyboard
shortcut).
Both flags are **UI-affordance flags only**: the server does not block
create/edit mutations based on them, so the system, API, and workflows
continue to mutate these records freely. They are orthogonal statements
about the object's nature with no implication rule in the data model.
Because today's inline creation UX creates a blank record the user must
then edit, the frontend create predicate currently requires both
`isUICreatable` and effective editability.
There is no CREATE permission in `ObjectPermissions`; the frontend keeps
gating creation on `canUpdateObjectRecords` as a proxy, ANDed with the
new flags.
## Unified create predicate
All generic creation entry points now flow through one predicate,
`canCreateRecordsForObjectMetadataItem` (`isUICreatable` && not
`isSystem` && not effectively read-only, where effective read-only
covers `isUIEditable`, `isRemote`, and the `canUpdateObjectRecords`
proxy via `isObjectMetadataReadOnly`). This deletes the previously
hardcoded suppression lists:
- `isRecordTableCreateDisabled.ts` and its hardcoded
`WorkflowRun`/`WorkflowVersion` list — deleted; those objects (plus
`workspaceMember`) now declare `isUICreatable: false` in the standard
application instead.
- The hardcoded `workspaceMember` guard inside
`useAddNewRecordAndOpenSidePanel.ts` — deleted.
- The `CREATE_NEW_RECORD` command menu item's availability expression
now checks `objectMetadataItem.isUICreatable`, `isUIEditable`,
`isSystem`, and `isRemote`; a workspace upgrade command re-syncs the
expression in existing workspaces.
Component-local conditions (soft-delete filter active, layout
customization mode) stay in their components.
## GraphQL compatibility and removal plan
The schema delta versus main is **purely additive plus deprecations —
zero breaking changes**:
- `isUIReadOnly` remains on both the ObjectMetadata and FieldMetadata
GraphQL output types for **one release** as a deprecated field computed
as `!isUIEditable` (`deprecationReason: 'Use isUIEditable'`). The Twenty
frontend no longer queries it.
- `isUIReadOnly` also remains on the **input side** for one release
(`CreateFieldInput`, `UpdateFieldInput`, `FieldFilter`, `ObjectFilter`),
keeping the schema shape identical to main for those members. On create
it acts as a legacy alias mapped to `!isUIReadOnly` (`isUIEditable` wins
when both are provided); on update it is ignored, exactly as on main (it
was never an editable property). Filtering on the deprecated member
keeps working until the column is dropped at upgrade time; after that it
is a deprecated no-op surface kept only for schema compatibility.
**Removal plan for next release: drop `isUIReadOnly` from the output
DTOs (and resolvers' `@ResolveField`s), from the input/filter types,
from the create-input mapping, and the `@WasRemovedInUpgrade`-retained
entity columns and decorators.**
## ⚠️ Webhook / database-event payload shape change
The `database-event-payload` type in `twenty-shared` got a clean rename
(no alias): metadata snapshots in webhook and database-event payloads
now carry `isUIEditable` (and `isUICreatable` at object level) **instead
of** `isUIReadOnly`, with inverted polarity. Consumers of these payloads
that read `isUIReadOnly` must switch to `isUIEditable`.
## New manifest properties (app-developer DX)
Application developers can now set these flags in their app manifests
(purely additive — existing manifests and older `twenty-sdk` versions
are unaffected, defaults apply when omitted):
- `objects[].isUICreatable?: boolean` (default `true`)
- `objects[].isUIEditable?: boolean` (default `true`)
- `fields[].isUIEditable?: boolean` (default `true`)
The manifest converters previously hardcoded `isUIReadOnly: false`; they
now read the manifest values with `?? true` defaults. The types are
re-exported through `twenty-sdk` from `twenty-shared`.
## Migration & backfill
- One fast instance command: adds `isUIEditable` (NOT NULL default
`true`) on `core."objectMetadata"` and `core."fieldMetadata"`, backfills
`isUIEditable = false` exactly where `isUIReadOnly = true`, drops
`isUIReadOnly`, and adds `isUICreatable` (default `true`) on
`objectMetadata`. The `down` is the exact inverse. Uses `ADD/DROP COLUMN
IF (NOT) EXISTS`, matching the 2-12 drop-`isCustom` precedent. Verified
up and down in separate transactions against a dev database with exact
backfill counts.
- **Cross-version upgrade safety (multi-version self-hosted jumps):**
the upgrade sequence interleaves per version (instance → workspace
commands), so pre-2.13 workspace commands run **before** the 2.13 rename
when an old instance jumps several versions. Following the `isCustom`
precedent: `isUIEditable`/`isUICreatable` are marked
`@WasIntroducedInUpgrade` and `isUIReadOnly` stays on both entities as
`@WasRemovedInUpgrade`, so the upgrade-aware entity metadata adapter
hides the not-yet-existing columns (and keeps the legacy column live) at
pre-2.13 cursors. **No committed upgrade command outside the 2-13
directory is modified**: the old 1-21/2-8/2-9 commands keep their
original `isUIReadOnly: true` inputs, which still compile (entity
property retained, deprecated create-input alias mapped) and still
produce the correct legacy column writes pre-rename.
- A 2-13 workspace command (`sync-standard-ui-capability-flags`)
re-syncs `isUICreatable` **and** `isUIEditable` on standard objects and
`isUIEditable` on standard fields from the standard-application
definitions. This backfills `isUICreatable: false` on
`workflowRun`/`workflowVersion`/`workspaceMember` and heals fields
created mid-cross-upgrade by pre-2.13 commands (whose hidden
`isUIEditable` value cannot reach the insert). Both 2-13 sync commands
pass `isSystemBuild: true` — the flat metadata validator otherwise
rejects direct updates to system objects (verified against a
deliberately drifted dev database; the run is idempotent).
- A second 2-13 workspace command re-syncs the create-record command
availability expression.
## Testing
- Unit tests for `canCreateRecordsForObjectMetadataItem`
(flag/permission/system combinations) and for the manifest converters
(flags set / omitted → defaults).
- Full `upgrade --dry-run` boots the sequence (107 steps) and validates
the upgrade-aware decorator references; both 2-13 sync commands verified
end to end against real drift and re-run idempotently.
- Schema verified by live introspection after the input-alias restore:
all four input/filter members match main, output deprecations intact;
frontend metadata types and `twenty-client-sdk` schema regenerated from
the running server.
- Read-only-related and touched jest suites pass on both packages;
typecheck and lint pass on `twenty-server` and `twenty-front`.
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a
href="https://cursor.com/agents/bc-0f3e04cb-b04a-40be-8330-5609c4538e8a"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a
href="https://cursor.com/background-agent?bcId=bc-0f3e04cb-b04a-40be-8330-5609c4538e8a"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21504?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
|
||
|
|
303c415dd1 |
fix(ai) - add logs + remove dashboard building (#21440)
- add logs for thread finishing without agent message - add logs to monitor toolCall token usage - remove dashboard building via AI (before fixing it) - fix Anthropic compute |
||
|
|
b56fea69aa |
fix(ai) - optimize metadata CRUD tools (#21235)
Reduces output tokens for all 13 metadata tools by (~49%) based on
production sampling data.
GET tools (field + object metadata)
System fields are now returned as compact {id, name, type} instead of
the full ~20-key payload (opt-in includeFullSystemFields to get full
payload). System objects are similarly compacted to {id, nameSingular,
namePlural}.
Internal fields the agent never uses (searchVector, deletedAt, position,
updatedBy) are excluded entirely.
workspaceId and applicationId are hoisted into a response envelope
instead of being repeated on every record.
Null/default-false properties are stripped from custom field and object
payloads (e.g. options: null, settings: null, isUIReadOnly: false).
CUD tools (create/update/delete)
Create and update field tools now return {id, name, type, label} instead
of the full DTO.
Create and update object tools now return {id, nameSingular,
labelSingular} instead of the full DTO.
Delete tools return {id, success: true} instead of the full DTO of the
deleted entity.
Validation errors are grouped by message — e.g. 10 fields failing the
same check produce one line with all names instead of 10 identical
lines.
Learn schemas (all tools)
UUID pattern regex stripped from JSON schemas (keeps format: "uuid").
$schema and additionalProperties: false stripped from all generated
schemas.
All Zod .describe() annotations and tool descriptions shortened.
Skill & tool description updates:
All references to the removed list_object_metadata_items tool replaced
with get_object_metadata / get_field_metadata across skill instructions,
dashboard tools, view filter/sort tools, and MCP server instructions.
|
||
|
|
8bd4cbc3fd |
fix(ai) - optim (#21233)
1. tool-registry.service.ts, Pass precomputed catalog to resolveSchemas() resolveSchemas() now accepts an optional precomputedCatalog parameter. Both getToolsByName() and getToolInfo() pass the catalog they already fetched, eliminating a redundant getCatalog() rebuild inside resolveSchemas(). 2. database-tool.provider.ts, Skip field lookup when schemas=false When building the catalog index (includeSchemas=false), getFlatFieldsFromFlatObjectMetadata() is no longer called for each of the 25 objects. The hasGroupByToolInputSchema() check is also skipped, group_by tools are always included in the index, with the real eligibility check deferred to learn_tools time. --> 100/150ms gain on learn/execute_tool execution |
||
|
|
15eaabdbc1 |
fix(ai) - optimize crud tools (#21133)
- **Add delete many**, `delete_many_{object}` added alongside the
existing `delete_one_{object}`.
- **Uniformize naming**, crud module, type names, and MCP helper
constants renamed for consistency.
- **Optimize tool schema (learn phase)**
- `find_many(_companies)`: **7 158 → 2 700 tokens**
- `find_one(_company)`: **280 → 126 tokens**
- ....
- Main mechanism: `reused: 'ref'` (line 7 of
`to-tool-json-schema.util.ts`). Zod walks the schema tree, tracks which
Zod schema instances appear more than once, and emits each reused
instance exactly once in `$defs`, replacing all subsequent occurrences
with a `$ref`. Works because filter and value schemas are now extracted
as shared objects.
- **Optimize system prompt (tool catalog)**, DATABASE_CRUD section
restructured to list operation patterns (`find_many_{object}`, …) once +
objects once, instead of the full N×M cross-product of tool names.
- **Optimize execute_tool**, shared record-properties schema (same
`$defs` deduplication applies at call time); introduced `upsert_many`;
added `selectedFields` to `find_*` so the agent only fetches the fields
it needs.
|
||
|
|
dd0039ca1c |
feat(mcp) - optimize instruction prompt and hide get_tool_catalog (#21183)
Workspace-aware initialize.instructions - Deleted the static mcp-server-instructions.const.ts - Created build-mcp-server-instructions.util.ts — a comprehensive system prompt with identity, object list, tool grammar, routing decision tree, intent mapping, skills vs tools, safety constraints, and data efficiency guidelines - Created McpInstructionBuilderService — fetches workspace-specific object names + skill names and injects them into the instructions Hide/deprecate get_tool_catalog Benefit : skip first MCP call (tools are included in instruction) |
||
|
|
7fcbc45017 |
perf(ai) - per-tool schema resolution in resolveSchemas (#21188)
learn_tools(["find_many_companies"]) goes from ~170ms → ~2ms (85x faster, measured with 40 objects / empty fields — real workspaces would see even bigger savings). Previously it generated schemas for all ~250 tools and discarded 249; now it generates exactly the requested one(s). |
||
|
|
1ae00d6753 |
fix(ai): correct find-records tool description (top-level filter fields) (#21109)
## Problem
The AI `find_<object>` tool builds its input schema with
`generateFindToolInputSchema`, which **spreads field filters at the args
root** (alongside `limit`/`offset`/`orderBy`/`and`/`or`/`not`).
`tool-executor.service.ts` then maps the raw model args to a filter
with:
```ts
const { limit, offset, orderBy, ...filter } = args;
```
The zod schema is only used to generate the JSON schema *shown* to the
model (`z.toJSONSchema(...)`) — it is **never used to validate the args
coming back**. So when the model emits a bare operator where a field
name belongs, e.g. `{ ilike: "Foreman" }`, it passes straight to the
query runner, which throws and burns a retry mid-turn:
```
ERROR [FindRecordsService] Failed to find records: Object person doesn't have any "ilike" field.
ERROR [FindRecordsService] Failed to find records: Object person doesn't have any "eq" field.
```
Two contributing faults:
1. **The tool description actively misleads the model** — it says ``use
filter: { id: { eq: "record-id" } }``, a `filter` wrapper the
root-spread schema doesn't have, inviting the malformed shape.
2. **No server-side validation** — invalid root keys reach the query
runner instead of being rejected against the advertised contract.
## Fix
1. **`FindRecordsService` prunes invalid filter keys before querying.**
Using the same filter shape the tool schema advertises
(`generateRecordFilterSchema(...).filterShape`), it drops any key that
is neither a real field nor a logical operator (`and`/`or`/`not`),
recursing through `and`/`or`/`not`. A model that sends `{ ilike:
"Foreman" }` now gets a valid (empty) filter rather than an exception.
Extracted as a pure, unit-tested util `pruneFilterToAllowedKeys`.
2. **Corrected the `find_<object>` tool description** to describe the
real top-level-field shape and explicitly warn against a `filter`
wrapper and bare root operators.
## Test
`__tests__/prune-filter-to-allowed-keys.util.spec.ts` covers: valid
filters untouched, bare root operators dropped, valid siblings
preserved, `and`/`or`/`not` recursion, and non-object input.
## Notes
- Defensive for all `FindRecordsService` callers; `find_one` (`{ id: {
eq } }`) and workflow find-records pass valid filters and are
unaffected.
- Companion to #21106 (RICH_TEXT composite filters). Both surfaced from
the same `"Tom Foreman's notes"` AI-chat repro; this PR addresses the
root-level-operator half.
---------
Co-authored-by: Rich Roberts <rich.roberts@talentpipe.ai>
|
||
|
|
996cdaf3ff |
refactor(agents): split tool resolution into native and action rails (#20331)
## Summary
Splits AI agent tool resolution into two independent rails:
- **Native tools** — capabilities baked into the model SDK
(Anthropic/OpenAI `web_search`, xAI `web`/`x` provider options). Bound
by `NativeToolBinderService`, controlled by per-agent
`modelConfiguration` toggles. Opaque to Twenty — executed on the model
provider's servers.
- **Action tools** — registry-scoped tools from `ToolRegistryService`
(code interpreter, send email, record CRUD, etc.). Permission-gated via
the agent's role. Executed on Twenty's server.
Both rails merge into a single `ToolSet` at call time. When both
surfaces expose a search tool the model picks at runtime — coexistence
is intentional (relevant once Exa returns as an action, see below).
## Notable changes worth calling out
**Contract change: `AgentAsyncExecutorService.executeAgent` no longer
accepts `rolePermissionConfig`.** Workflow agents now scope exclusively
by the agent's own permission-tab role (`unionOf: [agentRoleId]`). The
previous role-merging path (caller role intersected with agent role) is
removed. No agent role → no registry tools (fail-closed by design).
**`NativeToolBinderService` relocated** from
`core-modules/tool-provider/native/` →
`metadata-modules/ai/ai-models/services/`. The binder needs SDK-package
knowledge, which lives in `ai-models`. Old location created a backwards
module dependency.
**`NATIVE_MODEL_TOOLS_BY_SDK_PACKAGE` is exhaustive over
`AiSdkPackage`** (`Record<>`, not `Partial<Record<>>`). Adding a new SDK
without thinking about native tools now fails the build. SDKs without
native tools (Bedrock, Google, Mistral, Azure, OpenAI-compatible) get
explicit `{}` entries.
**Discriminated union `kind: 'sdk-tool' | 'provider-option'`** lets one
registry describe both function tools (Anthropic/OpenAI) and runtime
sources (xAI). Follows the local `tool-provider` convention from #19321.
## Deferred to follow-ups
- **Exa web search is dropped from this PR** (along with its
`WEB_SEARCH_TOOL` permission flag and the Exa-specific gating). Exa
comes back as an **action/app tool** once apps can define permission
flags through the SDK — ongoing work in #20481.
- **xAI native search currently errors.** xAI deprecated its Live Search
API (the `web`/`x` provider-option sources this rail maps to), so xAI
returns `410` when native search is actually exercised. The code path
itself is clear — it's only hit if you test xAI native tools. Fixed
separately alongside the broader xAI model fixes.
## Conscious non-decisions
- **No "twenty-native" category.** `native` is reserved for
model/provider SDK features; everything Twenty-owned is just a
tool/action.
- **Coexistence over precedence.** No rule forcing an action search tool
to override native search (or vice-versa) — when both exist, it's the
user's choice in workflow agents and the model's choice in chat.
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
|
||
|
|
de7daaa81a |
fix: exclude system objects and workflow/dashboard from AI/MCP write tool descriptors (#20973)
## Summary fix: exclude system join objects from AI/MCP create/update/delete tool descriptors Closes #20403 --- AI was used for assistance. --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
de044f4b45 |
feat(ai-chat): add navigation menu item + webhook tool providers (#20759)
## Summary
Exposes two Twenty primitives to the AI chat that it could not
previously manage:
- **Navigation menu items** — workspace nav and personal favorites
(favorites are just nav items with `scope: 'user'`).
- **Webhooks** — full CRUD with a structured operations input (record +
metadata events).
Page layouts and workflow runs were originally in this PR but have been
split out — they touch heavier surfaces (21 widget configurations and
the workflow runner cycle, respectively) and deserve their own focused
PRs.
### Tool inventory (8 new tools across 2 providers)
| Provider | Tools |
|---|---|
| NavigationMenuItem | `list_`, `create_`, `update_`,
`delete_navigation_menu_item` |
| Webhook | `list_`, `create_`, `update_`, `delete_webhook` |
### Design notes
- Both providers follow the established **view-style pattern**: tool
workspace service lives in the entity module's `tools/` folder, is
provided + exported by the entity module, and `ToolProviderModule`
imports the entity module. No `@Global()` modules or injection tokens
introduced.
- `create_navigation_menu_item` uses a Zod `discriminatedUnion` on
`type` (`FOLDER` / `LINK` / `OBJECT` / `VIEW` / `RECORD` /
`PAGE_LAYOUT`). `scope: 'workspace' | 'user'` switches between shared
nav and personal favorites — the underlying
`NavigationMenuItemAccessService` enforces LAYOUTS for workspace writes.
- Webhook operations accept both record events (`{kind:'record', object,
event}` → `<object>.<event>`) and metadata events (`{kind:'metadata',
metadataName, operation}` → `metadata.<metadataName>.<operation>`).
- Permissions reuse existing flags (`LAYOUTS`, `API_KEYS_AND_WEBHOOKS`).
No new permission flags, no migrations.
### Category cleanup
- New: `ToolCategory.NAVIGATION_MENU_ITEM`, `ToolCategory.WEBHOOK`.
- `ToolCategory.VIEW_FIELD` → folded into `VIEW`. Same permission gate,
same domain — separate category was organizational drift.
- `navigate_app` action stays in `ToolCategory.ACTION` where it belongs.
### System prompt addition
[chat-system-prompts.const.ts](packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/constants/chat-system-prompts.const.ts)
now teaches the AI:
- Favorites are nav items with `scope: 'user'`.
- A default OBJECT nav item is auto-created with
`create_object_metadata` — don't double-create.
### One file = one export
Every new schema / type / util file has exactly one top-level export.
## Test plan
- [ ] `npx nx typecheck twenty-server` — passes
- [ ] Spin up locally and exercise via AI chat:
- [ ] "Pin the Companies view to my favorites in a folder called
Important." → `create_navigation_menu_item` (FOLDER, user) then (VIEW,
user, folderId)
- [ ] "Register a webhook to https://example.com firing when any person
is created or updated." → `create_webhook` with discriminated operations
- [ ] Verify workspace-scoped nav writes are denied for a user without
LAYOUTS permission
- [ ] Verify user-scoped nav writes work without LAYOUTS permission
## Follow-ups (separate PRs)
- Page layout tools (record-page, record-index, standalone) — needs
widget-config strategy.
- Workflow run tools (list, get, run, stop) — uses the workflow-runner
cycle path.
- Dashboard / page-layout tool unification —
`DashboardToolWorkspaceService` and a future
`PageLayoutToolWorkspaceService` both inject the same trio
(PageLayout/Tab/Widget services).
- Webhook Settings page reads from raw Apollo query — switch to the
metadata store so it refreshes when the AI mutates webhooks.
|
||
|
|
53fdac1417 |
feat(apps): split AI tool and workflow action triggers in LogicFunction manifest (#20208)
## Summary Replaces the bolted-on `isTool` + `toolInputSchema` fields on `LogicFunctionManifest` with two distinct, opt-in triggers that align with the existing `cron` / `databaseEvent` / `httpRoute` trigger pattern: - **`toolTriggerSettings`** — exposes the function as an AI tool (chat / MCP / function calling). Uses standard JSON Schema (the format LLMs natively understand). - **`workflowActionTriggerSettings`** — exposes the function as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper `FieldMetadataType`-aware editors, variable pickers, labels, and an optional `outputSchema`. A function can opt into none, one, or both. Each surface gets the schema format appropriate for it. ### Why `isTool: true` previously exposed the function as both an AI tool AND a workflow node, with the same JSON Schema feeding both — but the workflow builder really wants Twenty's `InputSchema` (with `CURRENCY`, `RELATION`, `EMAILS`, etc.) and the AI surface really wants standard JSON Schema. Today the workflow builder hacks around this by treating JSON Schema as `InputSchema`, which silently breaks for any non-primitive field type. Splitting the triggers fixes that and lets each surface evolve independently. ### Migration - **Fast** instance command adds the two new nullable columns. - **Slow** instance command backfills `toolTriggerSettings` + `workflowActionTriggerSettings` from `isTool=true` rows (preserving today's both-surfaces behaviour) then drops the legacy columns. ### Stacked Stacked on top of #20181. Merge that first, then this. ## Test plan - [ ] CI green (oxlint, typecheck, jest, vitest) - [ ] Run `--include-slow` upgrade against a workspace with existing `isTool=true` logic functions; verify both new columns populated and old columns dropped - [ ] Verify AI chat sees migrated tool functions (Linear create-issue, Exa search) and can call them with the JSON Schema - [ ] Add an AI-tool function from the Settings UI (toggles `toolTriggerSettings`) and verify it shows up in chat - [ ] Add a workflow-action function from the Settings UI (toggles `workflowActionTriggerSettings`) and verify it appears in the workflow node picker - [ ] In the workflow builder, edit a `LOGIC_FUNCTION` step and verify input fields render (no more JSON-Schema-as-InputSchema hack) - [ ] Try defining a function with no triggers in the SDK and verify `defineLogicFunction` rejects it 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: martmull <martmull@hotmail.fr> |
||
|
|
bb5f294c5a |
[AI] Collapse NativeToolBinder to a single bind() entry (#20051)
The binder doesnt need an agent or full tool context -- just a model and options. Single `bind(model, options)` entry! Builds on #20022. |
||
|
|
d1a4902460 |
[AI] Drop 'serialization' from tool output naming (#20052)
didnt made sense anymore -- we dont really 'serialize' |
||
|
|
6c1c0737b0 |
Clarify registry tools vs native model tool binding (#20022)
## Intent This is a small foundation cleanup for the tool architecture. The main decision is: registry tools and native SDK/model tools are different things. - Registry tools have descriptors, schemas, catalog entries, and execute through `ToolExecutorService` - Native model tools are opaque AI SDK objects, bound directly into the model `ToolSet` - Surfaces still own their policy: chat, MCP, and workflow agents decide what they expose ## What changed - Removed `NATIVE_MODEL` from `ToolCategory` - Kept `ToolRegistryService` focused on registry-backed tools only - Moved native model tool binding through `NativeToolBinderService` - Reused native binding from chat instead of duplicating provider-specific web-search logic - Kept MCP local execution exclusions in a dedicated constant - Moved surface-specific constants into dedicated constant files ## What comes next - Move hardcoded chat app preloads, like Exa web search, into app/manifest metadata - Decide a clearer policy for local runtime tools like code interpreter and HTTP request - Gradually document the three tool shapes: registry tools, native model tools, and local runtime tools --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
4f938aa097 |
feat(app): infrastructure for pre-installed apps (#19973)
**PR 1 of 2.** Follow-up PR ships the Exa app, sets it as a default
pre-installed app, and removes the current `WebSearchTool` /
`WebSearchService` / `ExaDriver`. This PR adds the plumbing; no
user-visible change yet.
## Summary
- Server admins can declare a list of npm app packages to auto-install
on every new workspace and backfill onto existing workspaces via CLI.
- Server-level secrets (like Exa's API key) live on the
`ApplicationRegistration` (one row per server, encrypted) and are
injected into logic function execution env at runtime. No more
per-workspace storage of global secrets.
- A generic `POST /app/billing/charge` endpoint lets app logic functions
emit workspace usage events for metered features. Exa uses it in PR 2;
future apps (call recorder, etc.) reuse it.
- `LogicFunctionToolProvider` tool name prefix changes `logic_function_`
→ `app_`. Shorter, accurate (they come from installed apps).
## What's in this PR
**Logic function executor — server-level variables**
- `LogicFunctionExecutorService.getExecutionEnvVariables` now resolves
env vars in the order: hardcoded defaults →
`ApplicationRegistrationVariable[]` (server-level) →
`ApplicationVariable[]` (workspace-level override). The manifest
`serverVariables` schema has existed; this closes the loop.
**Config**
- `PRE_INSTALLED_APPS` — comma-separated list of npm packages. Default:
empty.
**\`PreInstalledAppsService\`** (new module)
- \`onApplicationBootstrap()\` — fetches each package's manifest from
the app registry CDN, upserts an \`ApplicationRegistration\`, and seeds
declared \`serverVariables\` from matching env vars (e.g.
\`EXA_API_KEY\` env → encrypted registration variable).
- \`installOnWorkspace(workspaceId)\` — installs all pre-installed apps
on a single workspace. Tolerates per-app failures.
**Auto-install on new workspace activation**
- \`WorkspaceService.prefillCreatedWorkspaceRecords\` invokes
\`installOnWorkspace\` after prefilling standard records. Non-blocking
on failure.
**Backfill CLI command**
- \`install-pre-installed-apps\` — iterates active and suspended
workspaces, installs pre-installed apps that aren't yet installed.
Idempotent. Run after changing \`PRE_INSTALLED_APPS\`.
**App billing endpoint**
- \`POST /app/billing/charge\`. Authenticated via \`APPLICATION_ACCESS\`
token (already injected into logic function execution env as
\`DEFAULT_APP_ACCESS_TOKEN\`). Body: \`{ creditsUsedMicro, quantity,
unit, operationType, resourceContext? }\`. Emits \`USAGE_RECORDED\` with
\`applicationId\` as \`resourceId\`. Generic — reusable by any app.
**Tool name prefix**
- \`LogicFunctionToolProvider.buildLogicFunctionToolName\` now produces
\`app_<name>\` instead of \`logic_function_<name>\`. Only affects tools
sourced from logic functions; other tool providers unchanged.
## Stats
- 16 files, +501 / −2
- 7 new files (1 command, 1 service × 2, 1 controller, 1 DTO, 2 modules)
- Typecheck: 7 pre-existing errors, zero new
- Prettier clean
## Behavior deltas
- **\`PRE_INSTALLED_APPS\` default = empty**: existing servers see no
change on merge.
- **\`ApplicationRegistrationVariable\` is now read by the executor**:
apps that were using manifest \`serverVariables\` but expecting them to
be ignored by the executor will now see them injected. No apps ship with
\`isTool: true\` logic functions today, so this is latent — first
consumer is Exa in PR 2.
- **Tool prefix**: currently no logic-function tools are named
\`logic_function_*\` in any production flow. The prefix change affects
only future tools emitted by \`LogicFunctionToolProvider\`.
## Risks
- **CDN unavailability at startup**: if the app registry CDN is down,
\`ensureRegistrationsExist\` logs warnings but doesn't block server
start. Installation on new workspaces during this window will find no
registrations and log a non-blocking error. Backfill command can retry
after CDN recovers.
- **Cold-start overhead**: \`ensureRegistrationsExist\` is called once
per process on bootstrap. Current configurable default is empty, so zero
overhead. When an admin sets \`PRE_INSTALLED_APPS\`, they accept one
HTTP call per package at boot.
- **Server-level variables flow**:
\`ApplicationRegistrationVariable.encryptedValue\` is shared by all
workspaces of a server. Appropriate for a single-tenant Exa key. Not
appropriate for per-tenant keys — those go in workspace-level
\`ApplicationVariable\` and override.
## Test plan
- [ ] \`npx nx typecheck twenty-server\` passes (verified: 7
pre-existing unrelated errors, zero new)
- [ ] Set \`PRE_INSTALLED_APPS=@twenty-apps/hello-world\` (or any real
npm-published app), \`HELLO_WORLD_API_KEY=xxx\`, restart server:
\`ApplicationRegistration\` row is upserted,
\`ApplicationRegistrationVariable\` for HELLO_WORLD_API_KEY is populated
(encrypted).
- [ ] Create a new workspace: the app is auto-installed,
\`ApplicationEntity\` row created, \`LogicFunctionEntity\` rows created.
- [ ] Existing workspace: run \`yarn nx run twenty-server:command
install-pre-installed-apps\`: apps install across all workspaces,
idempotent on re-run.
- [ ] Trigger a logic function that reads
\`process.env.HELLO_WORLD_API_KEY\`: value resolves from the
server-level \`ApplicationRegistrationVariable\`.
- [ ] Log a charge from the handler: \`POST /app/billing/charge\` with
\`Authorization: Bearer \$DEFAULT_APP_ACCESS_TOKEN\` body
\`{creditsUsedMicro: 1000, quantity: 1, unit: "INVOCATION",
operationType: "WEB_SEARCH"}\` → returns \`{success: true}\`,
\`USAGE_RECORDED\` event emitted with correct
\`resourceId=applicationId\`.
- [ ] Tool name generated by \`LogicFunctionToolProvider\` starts with
\`app_\`.
## What's NOT in this PR (PR 2 scope)
- The Exa app itself (\`packages/twenty-apps/...\` directory)
- Removing \`WebSearchTool\`, \`WebSearchService\`, \`ExaDriver\`,
\`web-search\` module
- Removing \`WEB_SEARCH_DRIVER\` config var
- Removing the current \`exa_web_search\` entry in
\`ActionToolProvider\`
- Chat preload list updated to \`app_exa_web_search\`
- Frontend \`getToolDisplayMessage\` branch for \`app_exa_web_search\`
- Setting \`PRE_INSTALLED_APPS\` default to include \`@twenty-apps/exa\`
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0c929e7903 |
refactor(tool-provider): rename web_search to exa_web_search, drop XOR toggle (#19969)
## Summary
- Today `WEB_SEARCH_PREFER_NATIVE` forces a **mutual exclusion**: either
the custom Exa tool preloads as `web_search` or the SDK-native
`web_search` binds. Same name, different backends.
- This PR lets them **coexist**. Custom Exa becomes `exa_web_search`;
native keeps `web_search`. The model picks based on tool descriptions.
- `WEB_SEARCH_PREFER_NATIVE` and `shouldUseNativeSearch()` are deleted.
Exa enablement follows `WEB_SEARCH_DRIVER` (existing). Native enablement
follows the agent's `modelConfiguration.webSearch.enabled` (existing).
## Key changes
**Config / service**
- Deleted `WEB_SEARCH_PREFER_NATIVE` (config-variables.ts)
- Deleted `WebSearchService.shouldUseNativeSearch()`
- `WebSearchService.isEnabled()` unchanged — still gates Exa
availability
**Custom tool rename**
- `ActionToolProvider.toolMap`: `'web_search'` → `'exa_web_search'`
- Descriptor name matches
- `WebSearchTool.description` rewritten to position Exa as
structured/entity-aware, complementary to native
**Native tool binder**
- `NativeToolBinder.bind()` drops the `shouldUseNativeSearch` gate.
Per-agent `modelConfiguration.webSearch.enabled` (inside
`getNativeModelTools`) stays authoritative.
**Chat**
- Preload list now always includes `exa_web_search` —
`ActionToolProvider` silently skips the descriptor when Exa is disabled,
so `getToolsByName` degrades gracefully
- Native tools always attempted; returns empty ToolSet when the model
doesn't support them
- `directTools = { ...preloadedTools, ...nativeSearchTools }` — both
present when both enabled
- `billNativeWebSearchUsage` called unconditionally (the function
already short-circuits on count ≤ 0)
**Workflow agent**
- Same unconditional billing pattern
- `WebSearchService` dependency removed
**System prompt**
- Dropped the special-cased `web_search` branch. Preloaded tools list
uniformly now.
**Frontend**
- `exa_web_search` reuses the same "Searching the web for X" display as
native
- Test coverage added
## Billing isolation (verified)
- `countNativeWebSearchCallsFromSteps` counts `toolName ===
'web_search'` only. After the rename, only native calls match. Exa calls
(`exa_web_search`) are billed separately via
`WebSearchService.emitUsageEvent` inside `search()`.
- No double-billing path.
## Behavior deltas (intended)
| Scenario | Before | After |
|---|---|---|
| Anthropic model + Exa enabled + PREFER_NATIVE=true | native only |
**both** |
| Anthropic + Exa enabled + PREFER_NATIVE=false | Exa only (as
`web_search`) | **both** |
| Non-native model + Exa enabled | Exa as `web_search` | Exa as
`exa_web_search` |
| Any model + Exa disabled + native supported | native only | native
only |
| Workflow agent with `webSearch.enabled=true` + Anthropic + Exa enabled
| native only | **both** |
## Known regression (accepted)
Customers who set `WEB_SEARCH_PREFER_NATIVE=false` to force Exa-only
will now **also** see native `web_search` if the model supports it.
There's no chat-level kill switch after this PR. Per discussion, this is
accepted — future model-level capability gating (in the model JSON) will
be the right place for that control.
## Stats
- 10 files, +63 / −73 (net deletion)
- Typecheck clean (server: 7 pre-existing unrelated, front: 13
pre-existing unrelated — zero new either side)
- Prettier clean
## Test plan
- [ ] `npx nx typecheck twenty-server` and `npx nx typecheck
twenty-front` pass
- [ ] With Anthropic + Exa enabled: chat shows both `web_search` and
`exa_web_search` in preloaded list; model can call either
- [ ] With Anthropic + Exa disabled: chat shows only native `web_search`
- [ ] With non-native model + Exa enabled: chat shows only
`exa_web_search`
- [ ] Workflow agent with `modelConfiguration.webSearch.enabled=true` +
Exa enabled: both available
- [ ] Billing: native calls billed via `billNativeWebSearchUsage`; Exa
calls billed via `WebSearchService.emitUsageEvent`; no double-billing
- [ ] Frontend: `exa_web_search` renders "Searching the web for X" the
same as `web_search`
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
44309a6fd9 |
refactor(tool-provider): rename NativeModelToolProvider to NativeToolBinderService (#19966)
**Stacked on top of #19962.** ## Summary - `NativeModelToolProvider` lived under `providers/` and had the `*-tool.provider.ts` suffix, but it never implemented `ToolProvider`, wasn't in `TOOL_PROVIDERS`, had no descriptors, and wasn't executed by `ToolExecutorService`. The shape misled readers. - It's actually a **parallel concept**: a binder that produces SDK-native tool objects (Anthropic `webSearch`, OpenAI `webSearch`, etc.) which the AI SDK passes straight to the model. Opaque, not serializable, never in the catalog, never dispatched by the executor. - This PR renames + moves it to reflect that. ## Renames | Before | After | |---|---| | `NativeModelToolProvider` (class) | `NativeToolBinderService` | | `NativeToolProvider` (interface) | `NativeToolBinder` | | `generateTools(context)` (method) | `bind(context)` | | `providers/native-model-tool.provider.ts` | `native/native-tool-binder.service.ts` | | `interfaces/native-tool-provider.interface.ts` | `native/native-tool-binder.interface.ts` | ## What doesn't change - `ToolCategory.NATIVE_MODEL` enum stays (still used by `getToolsByCategories`). - `isAvailable()` signature unchanged. - `WebSearchService.shouldUseNativeSearch()` toggle untouched — that's product-level and belongs to a separate PR that handles the Exa coexistence story. - No behavior change. Pure rename + move. ## Why this matters for the broader architecture This rename makes the native/binder concept **visible in the type system and directory structure**. That's what later enables coexisting native + custom tools (e.g., `web_search` native alongside `exa_web_search` custom) without the current naming collision, because native tools are no longer masquerading as a registry provider. ## Stats - 5 files, +30 / −28. - Blast radius: 4 files modified, 1 file renamed (git tracks as rename). - Typecheck clean (7 pre-existing unrelated errors, zero new). - Prettier clean. ## Test plan - [ ] `npx nx typecheck twenty-server` passes - [ ] AI chat: native `web_search` still works end-to-end when enabled - [ ] Workflow AI agent: `ToolCategory.NATIVE_MODEL` still works (goes through `bind()` now) - [ ] MCP: unaffected (doesn't use native tools) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2a5d5b36db |
refactor(tool-provider): kill execute_tool's dual dispatch (#19962)
**Stacked on top of #19960.**
## Summary
- `execute_tool` used to check `directTools[toolName]` first, falling
back to the registry. Same tool name, different wrapping: preloaded went
through `wrapToolsWithOutputSerialization`, fallback didn't. Silent
divergence — a model calling a CRUD tool via
\`learn_tools\`/\`execute_tool\` got raw output, while calling it as a
preloaded direct tool got compacted output.
- Now: `execute_tool` always routes through
`toolRegistry.resolveAndExecute`. One path, no fast-path.
- Output serialization (`compactToolOutput`) moves into the registry,
gated by a new `serializeOutput` flag on `hydrateToolSet` /
`resolveAndExecute` / `getToolsByName` / `getToolsByCategories` /
`ToolRetrievalOptions`. Chat passes `true`, MCP and workflow pass
`false`.
## Key changes
**Registry (`tool-registry.service.ts`)**
- `hydrateToolSet` options gain `serializeOutput?: boolean`; when true
the execute closure wraps dispatch result with `compactToolOutput`.
- `resolveAndExecute` signature: replaces unused \`_options:
ToolExecutionOptions\` with `{ serializeOutput?: boolean }`.
- `getToolsByName` and `getToolsByCategories` thread `serializeOutput`
through to `hydrateToolSet`.
**Meta-tool (`execute-tool.tool.ts`)**
- API changes from positional `(toolRegistry, context, directTools?,
excludeTools?)` to `(toolRegistry, context, options?: { excludeTools?,
serializeOutput? })`.
- `directTools` fallback removed. All invocations go to the registry.
**Chat (`chat-execution.service.ts`)**
- Passes `serializeOutput: true` to `getToolsByName` — preloaded tools
get compacted output from the hydrator, no external wrap needed.
- Drops the external `wrapToolsWithOutputSerialization(preloadedTools)`
call.
- `createExecuteToolTool` call now passes `{ serializeOutput: true }`.
Direct-tool and `execute_tool` paths produce identical output shape.
**MCP (`mcp-protocol.service.ts`)**
- `createExecuteToolTool` call updated to new options shape with `{
excludeTools: MCP_EXCLUDED_TOOLS }`. No `serializeOutput` flag → raw
output as today.
**Deletes**
- `output-serialization/wrap-tools-with-output-serialization.util.ts` —
sole caller removed.
## Behavior changes
- **Chat, `execute_tool` fallback path**: now produces compacted output
(matches direct path). Net effect: fewer tokens for CRUD results reached
via discovery. Intended improvement.
- **Chat, `execute_tool({toolName: 'web_search'})` edge**: today
silently hits the native tool via `directTools`; now returns \"tool not
found, use get_tool_catalog\". Self-correcting, rare — native tools are
always directly available to the model.
- **MCP**: no change. No `serializeOutput` flag → identical raw output.
- **Workflow agent**: no change. Doesn't use `execute_tool`.
## Test plan
- [ ] `npx nx typecheck twenty-server` passes (verified: 7 pre-existing
unrelated errors, zero new)
- [ ] \`npx jest
packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts\`
passes in CI
- [ ] AI chat: call a preloaded tool (e.g. \`search_help_center\`)
directly → compacted output
- [ ] AI chat: call a non-preloaded CRUD tool via
\`learn_tools\`/\`execute_tool\` → compacted output (this is the
behavior change)
- [ ] AI chat: native \`web_search\` still works when model calls it
directly
- [ ] MCP: \`tools/call\` on a registry tool → raw output (nulls
preserved)
- [ ] Workflow AI agent: tool dispatch unchanged
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
b77c44fd20 |
refactor(tool-provider): dedupe descriptor/generator paths (#19960)
## Summary - Every tool provider used to implement `generateDescriptors()` **and** register a category generator at `onModuleInit()` that re-ran the same factories at execute time. `ToolExecutorService` carried two registries (`staticToolHandlers`, `categoryGenerators`) to route between them. - Providers now own execution of their own tools via a new `executeStaticTool()` method. `ToolExecutorService` drops both maps and delegates by `descriptor.category`. Each factory-backed provider has a single `buildToolSet()` used by both descriptor generation and execution. - Extracts `resolveObjectIcon` shared util (was duplicated verbatim in workflow + dashboard providers), and deletes the orphaned `ToolGeneratorModule` whose consumers were removed in the earlier AI chat simplification refactor. No behavior change. Same factories run, same permission checks, same tools execute. Net diff: 18 files, +311 / −480. ## Key changes - `ToolProvider` interface gains `executeStaticTool(name, args, context)`. - `ToolExecutorService` loses its `staticToolHandlers` and `categoryGenerators` maps, injects `TOOL_PROVIDERS`, and does `providers.find(p => p.category === descriptor.category).executeStaticTool(...)` for `kind: 'static'` descriptors. - `ActionToolProvider` drops the register-handler loop in its constructor; `executeStaticTool` looks up in the existing `toolMap`. - `View`, `Metadata`, `Workflow`, `Dashboard`, `ViewField` providers each have a single `buildToolSet(context)` private method used by both `generateDescriptors` and `executeStaticTool`. No more `onModuleInit`, no `ToolExecutorService` dependency. - `DatabaseToolProvider` and `LogicFunctionToolProvider` implement `executeStaticTool` with an invariant-violation throw — they only emit `database_crud` / `logic_function` kinds, so the static-tool path is unreachable for them. - Deletes `tool-generator/` (dead code — zero consumers). ## Dependency graph before/after **Before:** provider → `ToolExecutorService` (for `register*` calls) **After:** `ToolExecutorService` → `TOOL_PROVIDERS` → providers. Cleaner, no cycle. ## Test plan - [ ] `npx nx typecheck twenty-server` passes (verified: same 7 pre-existing unrelated errors) - [ ] `npx nx lint twenty-server` passes - [ ] AI chat: trigger a tool call that hits `execute_tool` fallback (e.g. a view/metadata tool not in the preloaded set) — verify it still executes - [ ] AI chat: trigger a preloaded action tool (e.g. `search_help_center`) — verify it still executes - [ ] MCP: `tools/list` and `tools/call` for both preloaded and catalog-discovered tools - [ ] Workflow AI agent: run a workflow with AI agent step that calls DATABASE_CRUD tools - [ ] Verify the `web_search` / `code_interpreter` tools (if enabled) still dispatch correctly --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
13afef5d1d |
fix(server): scope loadingMessage wrap/strip to AI-chat callers (#19896)
## Summary
MCP tool execution crashed with \`Cannot destructure property
'loadingMessage' of 'parameters' as it is undefined\` whenever
\`execute_tool\` was called without an inner \`arguments\` field. Root
cause: \`loadingMessage\` is an AI-chat UX affordance (lets the LLM
narrate progress so the chat UI can show "Sending email…") but it was
being wrapped into **every** tool schema — including those advertised to
external MCP clients — and \`dispatch\` unconditionally stripped it,
crashing on \`undefined\` args.
The fix scopes the wrap/strip pair to AI-chat callers only:
- Pair wrap and strip inside \`hydrateToolSet\` (they belong together).
- New \`includeLoadingMessage\` option on \`hydrateToolSet\` /
\`getToolsByName\` / \`getToolsByCategories\` (default \`true\` so
AI-chat behavior is unchanged).
- MCP opts out → external clients see clean inputSchemas without a
required \`loadingMessage\` field.
- \`dispatch\` no longer strips; args default to \`{}\` defensively.
- \`execute_tool\` defaults \`arguments\` to \`{}\` at the LLM boundary.
## Test plan
- [x] \`npx nx typecheck twenty-server\` passes
- [x] \`npx oxlint\` clean on changed files
- [x] \`npx jest mcp-protocol mcp-tool-executor\` — 23/23 tests pass
- [ ] Manually: call \`execute_tool\` via MCP with and without inner
\`arguments\` — verify no crash, endpoints execute
- [ ] Manually: inspect MCP \`tools/list\` response — verify
\`search_help_center\` schema no longer contains \`loadingMessage\`
- [ ] Regression: AI chat still streams loading messages as the LLM
calls tools
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6117a1d6c0 |
refactor: standardize AI acronym to Ai (PascalCase) across internal identifiers (#19837)
## Summary
The "AI" acronym was rendered inconsistently across the codebase. The
backend AI module had settled on PascalCase `Ai` (`AiAgentModule`,
`AiBillingService`, `AiChatModule`, `AiModelRegistryService`, etc.),
while frontend components, several DTOs, a few types, and shared
identifiers still used all-caps `AI` (`AIChatTab`,
`AISystemPromptPreviewDTO`, `SettingsPath.AIPrompts`, ...). CLAUDE.md
specifies PascalCase for classes; this PR normalizes everything internal
to `Ai`.
**This is a pure internal rename.** The GraphQL schema is untouched —
`@ObjectType` decorator string arguments, resolver method names (which
become Query/Mutation field names), gql template contents, and the
`generated-metadata/graphql.ts` file are preserved verbatim. The only
visible change is TypeScript identifiers and file names.
## Also folded in (adjacent cleanups)
- **`AgentModelConfigService` → `AiModelConfigService`**. Lives in
`ai-models/` and is used by multiple AI code paths, not just the Agent
entity. The "Agent" prefix was misleading.
- **`generate-text-input.dto.ts` → `generate-text.input.ts`**. The
`ai-agent/dtos/` folder already uses `<entity>.input.ts` convention for
Input classes (`create-agent.input.ts` etc.); the old path mixed
`.dto.ts` file extension with a class that has no DTO suffix. File
rename only; class stays `GenerateTextInput`.
- **Removed stale TODO** in `ai-model-config.type.ts` that asked for the
`AiModelConfig` rename that this PR performs.
## Rename methodology
Bulk rename via perl with anchored regex
`(?<!['"])(?<![A-Z.])AI([A-Z])(?=[a-z])/Ai$1/g`:
- **Lookbehind for non-uppercase** skips adjacent acronyms (`MOSAIC`,
`OIDCSSO`) and leaves `AIRBNB_ID` alone.
- **Lookbehind for non-quote** protects most string literals.
- **Lookahead for lowercase** restricts matches to PascalCase
identifiers (`AIChatTab`), leaving SCREAMING_SNAKE constants untouched.
Strict file-scope exclusions: `generated-metadata/**`, `generated/**`,
`locales/**`, `migrations/**`, `illustrations/**`, `halftone/**`, and
the two gql template files (`queries/getAISystemPromptPreview.ts`,
`mutations/uploadAIChatFile.ts`).
Post-rename reverts for identifiers where the regex was too eager:
- Backend resolver method names kept: `getAISystemPromptPreview`,
`uploadAIChatFile` (they are GraphQL field names).
- `@ObjectType('AdminAIModels')` / `('AISystemPromptPreview')` /
`('AISystemPromptSection')` kept as-is.
- Backend classes `ClientAIModelConfig` / `AdminAIModelConfig` kept
as-is (they use `@ObjectType()` with no argument, so the class name IS
the schema name).
- External-library symbols restored: `OpenAIProvider`,
`createOpenAICompatible`, `vercelAIIntegration`.
File renames use a two-step rename to work on macOS case-insensitive
filesystems: `git mv X.tsx X.tsx.tmp && git mv X.tsx.tmp renamed.tsx`.
## Diff audit
- 0 changes to migrations
- 0 changes to locale `.po` / `.ts` files
- 0 changes to `generated-metadata/graphql.ts`
- 0 changes to website illustration files (base64 blobs preserved)
- 0 renames inside user-facing translation strings (`t\`…\``,
`msg\`…\``, `<Trans>…</Trans>`)
## Test plan
- [x] `npx nx typecheck twenty-server` — PASS
- [x] `npx nx typecheck twenty-front` — PASS
- [x] `npx jest ai-model admin agent-role` — 79/79 PASS
- [x] `npx oxlint --type-aware` on 118 changed files — 0 errors
- [x] `npx prettier --check` on 118 changed files — clean
- [ ] CI
|
||
|
|
b284c8323c |
Remove Favorite and FavoriteFolder from workspace schema (#19536)
## Summary - Removes all workspace schema definitions for `Favorite` and `FavoriteFolder` entities, which have been fully migrated to `NavigationMenuItems` - Deletes 26 standalone files including workspace entities, NestJS modules, services, listeners, jobs, standard application builders (field metadata, views, view fields, view field groups, indexes, page layouts), mocks, and integration tests - Cleans up ~40 modified files: removes `favorites` relation from 10 workspace entities and their field metadata utils, removes entries from all builder maps, shared constants (`STANDARD_OBJECTS`, `CoreObjectNameSingular`, `DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS`), SDK default relations, AI tool filtering, and standard object icons |
||
|
|
f13e7e01fe |
[AI] Add group_by_* database tools and centralize groupBy validation (#19406)
closes https://discord.com/channels/1130383047699738754/1488990242873806868 https://github.com/user-attachments/assets/2b2bbfba-3fa6-4114-9a26-96a61599d748 <img width="729" height="1283" alt="CleanShot 2026-04-07 at 20 43 06" src="https://github.com/user-attachments/assets/815efb97-81a0-44ea-8d79-b3ce7d5b00b6" /> <img width="708" height="1266" alt="CleanShot 2026-04-07 at 20 40 13" src="https://github.com/user-attachments/assets/692366bc-b629-4d9f-b6b8-ab670d5ad046" /> <img width="665" height="3524" alt="CleanShot 2026-04-07 at 20 42 00" src="https://github.com/user-attachments/assets/5e844e0f-7835-47a8-9d20-a5baddc0992d" /> |
||
|
|
7ef80dd238 |
Add standard skills backfill and improve skill availability messaging (#19523)
## Summary This PR adds a database migration command to backfill standard skills for existing workspaces and improves the skill loading tool to provide dynamic, workspace-specific skill availability information instead of hardcoded skill names. ## Key Changes - **New Migration Command**: Added `BackfillStandardSkillsCommand` (v1.22.0) that: - Identifies missing standard skills in existing workspaces - Compares workspace skills against the standard skill definitions - Creates missing skills using the workspace migration service - Supports dry-run mode for safe testing - Properly logs all operations and handles failures - **Enhanced Skill Loading Tool**: Updated `createLoadSkillTool` to: - Accept a new `listAvailableSkillNames` function parameter - Dynamically fetch available skills from the workspace instead of using hardcoded skill names - Provide accurate, context-aware error messages when skills are not found - Gracefully handle workspaces with no available skills - **Service Updates**: Modified skill tool implementations in: - `McpProtocolService`: Integrated `findAllFlatSkills` to list available skills - `ChatExecutionService`: Integrated `findAllFlatSkills` to list available skills - **Module Registration**: Added `BackfillStandardSkillsCommand` to the v1.22 upgrade module - **Test Updates**: Updated `McpProtocolService` tests to mock the new `findAllFlatSkills` method ## Implementation Details The backfill command uses the existing workspace migration infrastructure to safely create skills, ensuring consistency with other metadata operations. The skill availability messaging now reflects the actual skills present in each workspace, improving user experience when skills are not found. https://claude.ai/code/session_012fXeP3bysaEgWsbkyu4ism Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
ea572975d8 |
feat: generic web search driver abstraction with Exa support and billing (#19341)
## Summary - Introduces a pluggable `WebSearchDriver` abstraction (interface, factory, service, module) so web search is no longer tied to native provider tools (Anthropic/OpenAI) - **Exa** is the first driver implementation with support for category-filtered search (company, people, news, research paper, etc.) — particularly useful for CRM workflows - Per-query billing for both Exa ($0.007/query) and native provider surcharges ($0.01/query for Anthropic/OpenAI) via the existing `USAGE_RECORDED` pipeline - New config variables: `WEB_SEARCH_DRIVER` (EXA/DISABLED), `EXA_API_KEY`, `WEB_SEARCH_PREFER_NATIVE` (default false — prefers Exa over native when both available) - `WEB_SEARCH` operation type added for usage tracking and Stripe metering ### Architecture ``` WebSearchDriver (interface) ├── ExaDriver — Exa neural search with category support └── DisabledDriver — throws when search is disabled WebSearchDriverFactory (extends DriverFactoryBase) └── creates driver based on WEB_SEARCH_DRIVER config WebSearchService (facade) ├── search(query, options?, billingContext?) ├── isEnabled() └── emits USAGE_RECORDED events per query WebSearchTool (Tool implementation) └── registered in ActionToolProvider, available via tool catalog ``` ### Native search billing gap fixed Anthropic and OpenAI both charge $0.01/search on top of token costs. The token costs were already billed, but the per-call surcharge was not. Added `countNativeWebSearchCallsFromSteps` utility + `billNativeWebSearchUsage` to `AiBillingService`, wired into both chat and workflow agent paths. ## Test plan - [ ] Set `WEB_SEARCH_DRIVER=EXA` + `EXA_API_KEY=...` and verify AI chat can search the web - [ ] Verify category parameter works (ask about a specific company/person) - [ ] Set `WEB_SEARCH_DRIVER=DISABLED` and verify search tool is not exposed - [ ] Set `WEB_SEARCH_PREFER_NATIVE=true` with Anthropic model and verify native search is used - [ ] Verify usage events are emitted in ClickHouse for both Exa and native search paths - [ ] Verify existing billing tests pass (`npx jest ai-billing.service.spec.ts`) Made with [Cursor](https://cursor.com) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
563e27831b |
Clean up tool output architecture: remove wrappers, enforce ToolOutput everywhere (#19321)
## Summary
- **Remove `ExecuteToolResult` wrapper** — `execute_tool` is now a
transparent dispatcher that returns the raw `ToolOutput` from underlying
tools. No more `{ toolName, result }` envelope.
- **Type the entire execution chain as `Promise<ToolOutput>`** — from
`ToolExecutorService.dispatch()` through `resolveAndExecute()` to
`execute_tool.execute()`. Zero `Promise<unknown>` remaining in the tool
layer.
- **Use `Extract<ToolExecutionRef, ...>`** for dispatch methods,
enabling exhaustive switch checking and removing `as never` casts.
- **Relax `ToolOutput.result` to accept `null`** — removes `??
undefined` hacks at the boundary with logic function results.
- **Enforce 1-export-per-file** across tool type/interface files (split
`tool-descriptor.type.ts`, `tool-provider.interface.ts`, `tool.type.ts`,
`tool-output.type.ts`, `tool-executor.service.ts`).
- **Simplify error handling** — `wrapWithErrorHandler` and all
meta-errors (tool not found, tool excluded) now return consistent
`ToolOutput` shape with `error` as a plain string.
- **Frontend reads output directly** — removed `unwrapToolOutput`
utility; `ToolStepRenderer` and `ThinkingStepsDisplay` extract
`message`/`error` from the raw output with simple type guards.
- **Add permission error detection** for email tools via
`isInsufficientPermissionsError`, guiding the AI model to suggest
account reconnection instead of hallucinating about visibility settings.
## Test plan
- [ ] AI chat tool calls return visible output (not "null") in the UI
- [ ] Tool errors display correctly in the JSON tree
- [ ] Email draft/send tools return actionable permission errors
- [ ] Code interpreter output renders correctly
- [ ] Thinking steps display tool outputs properly
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
||
|
|
bb3d556799 |
Refined demo workspace creation skill (rebased, review fixes) (#19274)
## Summary Rebased version of #19051 with all review comments addressed. Clean branch on latest main, lint/typecheck/tests passing. ### Changes from original PR - AI can now create, update, and delete **view filters** (`ViewFilterToolsFactory`) and **view sorts** (`ViewSortToolsFactory`) - `create_view` now accepts `calendarFieldName`, `calendarLayout`, and `fieldNames` to configure views at creation time - Three new standard skills: `view-building`, `view-filters-and-sorts`, `custom-objects-cleanup` - `workspace-demo-seeding` skill reworked to keep standard objects and enrich them with custom fields - Cache invalidation for nav menu items when object `isActive` changes - Dashboard tool descriptions improved (RECORD_TABLE widget workflow) ### Review comments addressed (all 10 from #19051) 1. **Sentry + Cubic**: Calendar field DATE/DATE_TIME validation — added `resolveCalendarFieldMetadataId` using `isFieldMetadataDateKind` 2. **Cubic**: "navigate tool" → "navigate_app tool" in skill metadata (all 7 occurrences) 3. **Copilot**: KANBAN views now require `mainGroupByFieldName` — throws clear error if missing 4. **Copilot**: CALENDAR views now require both `calendarFieldName` and `calendarLayout` — validated before DB call 5. **Copilot**: Mock field fixtures include `type` property (DATE_TIME, TEXT, SELECT) 6. **Copilot**: `ViewFilterValue` type assertion instead of unsafe `as string` casts (3 locations) 7. **FelixMalfait**: Removed `NavigationMenuItemObjectDeactivationListener` — replaced with cache invalidation 8. **FelixMalfait**: Consolidated `ViewFilterToolProvider` and `ViewSortToolProvider` into single `ViewToolProvider` 9. Removed `VIEW_FILTER` and `VIEW_SORT` from `ToolCategory` enum (merged into `VIEW`) 10. Removed stale `existingFeatureFlagsMap` param incompatible with current main ## Test plan - [x] `npx nx lint:diff-with-main twenty-server` — passes - [x] `npx nx typecheck twenty-server` — passes - [x] `view-tools.factory.spec.ts` — all 20 tests pass (including 3 new validation tests) Supersedes #19051 https://claude.ai/code/session_01QPV74NU6vzmJb32e4i899E --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
3c067f072c | [AI] Improve tools tab (#19221) | ||
|
|
223943550c |
[AI] Unify code-interpreter streaming rendering and fix assistant width jitter (#19235)
closes https://discord.com/channels/1130383047699738754/1480991390782455838 - Use data-code-execution as the streaming source of truth and hide duplicate code-interpreter tool parts (including tool-execute_tool wrappers). - Ensure wrapped execute_tool code-interpreter outputs still render correctly after refetch. - Gate code-interpreter server behavior by enablement state and keep assistant messages full-width to avoid streaming vs completed width shifts. Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
c407341912 |
feat: optimize hot database queries with multi-layer caching (#19068)
## Summary Introduces multi-layer caching for the 5 most frequent database queries identified in production (Sentry data), targeting the JWT authentication hot path and cron job logic. ### Problem Our database is under heavy load from uncached queries on the auth hot path: - `WorkspaceEntity` lookups: **638 queries/min** - `ApiKeyEntity` lookups: **491 queries/min** - `UserEntity` lookups: **147 queries/min** - `UserWorkspaceEntity` lookups: **143 queries/min** - `LogicFunctionEntity` lookups: **1800 queries/min** (cron job) ### Solution **1. New `CoreEntityCacheService`** for non-workspace-scoped entities (Workspace, User, UserWorkspace): - Mirrors `WorkspaceCacheService` architecture (in-process Map + Redis with hash validation) - Provider pattern with `@CoreEntityCache` decorator - Keyed by entity primary key (not workspaceId) - 100ms local TTL, Redis-backed hash validation for cross-instance consistency - Three providers: `WorkspaceEntityCacheProviderService`, `UserEntityCacheProviderService`, `UserWorkspaceEntityCacheProviderService` **2. New `apiKeyMap` WorkspaceCache** for workspace-scoped API key lookups: - `WorkspaceApiKeyMapCacheService` loads all API keys for a workspace into a map by ID - Leverages existing `WorkspaceCacheService` infrastructure - Cache invalidation on API key create/update/revoke **3. `CronTriggerCronJob` refactored** to use existing `flatLogicFunctionMaps` workspace cache: - Eliminates per-workspace `LogicFunctionEntity` repository queries (~1800/min) - Filters cached data in-memory instead **4. `JwtAuthStrategy` refactored** to use caches for all entity lookups: - Workspace, User, UserWorkspace → `CoreEntityCacheService` - ApiKey → `WorkspaceCacheService` (`apiKeyMap`) - Impersonation queries kept as direct DB queries (rare path, requires relations) **5. Cache invalidation** wired into mutation paths: - `WorkspaceService` → invalidates `workspaceEntity` on save/update/delete - `ApiKeyService` → invalidates `apiKeyMap` on create/update/revoke ### Architecture ``` Request → JwtAuthStrategy ├── Workspace lookup → CoreEntityCacheService (in-process → Redis → DB) ├── User lookup → CoreEntityCacheService (in-process → Redis → DB) ├── UserWorkspace lookup → CoreEntityCacheService (in-process → Redis → DB) └── ApiKey lookup → WorkspaceCacheService (in-process → Redis → DB) CronTriggerCronJob └── LogicFunction lookup → WorkspaceCacheService (flatLogicFunctionMaps) ``` ### Expected Impact | Query | Before | After | |-------|--------|-------| | WorkspaceEntity | 638/min | ~0 (cached) | | ApiKeyEntity | 491/min | ~0 (cached) | | UserEntity | 147/min | ~0 (cached) | | UserWorkspaceEntity | 143/min | ~0 (cached) | | LogicFunctionEntity | 1800/min | ~0 (cached) | ### Not included (ongoing separately) - DataSourceEntity query optimization (IS_DATASOURCE_MIGRATED migration) - ObjectMetadataEntity query optimization (already partially cached) |
||
|
|
2a82df7073 |
AI tools to create a demo workspace (#18236)
This PR adds the necessary tool to create a demo workspace with : relevant custom objects and fields, mock data and a real dashboard with graph widgets. It is still a bit under-optimized and slow but it works. This PR also adds an AI tool that allows to see what happens in real time, it navigates the app and waits when necessary. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
9107f5bbc7 |
feat: upgrade ai package to version six and the corresponding @ai-sdk/* packages to compatible versions (#18172)
Used the migration guide to carry out this upgrade: https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0 I have not been able to test locally due to credits. <img width="220" height="450" alt="image" src="https://github.com/user-attachments/assets/050b34b9-3239-4010-8c47-b43d44571994" /> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
66da296799 |
Unify MCP into single endpoint with lazy tool discovery (#18113)
## Summary - Consolidates the MCP server from two endpoints (`/mcp` + `/mcp/metadata`) into a **single `POST /mcp`** endpoint exposing five high-level tools: `get_tool_catalog`, `learn_tools`, `execute_tool`, `load_skills`, and `search_help_center` - Fixes **DATABASE_CRUD tools not accessible via API key auth** by removing an unnecessary `userId`/`userWorkspaceId` guard in `DatabaseToolProvider` and threading `ApiKeyWorkspaceAuthContext` through the tool context chain - Improves tool descriptions with **STEP 1/2/3 workflow guidance** so AI clients follow the correct discovery flow (catalog → learn → execute) instead of guessing tool names - Simplifies the **frontend AI settings** by removing the schema picker dropdown (no more "Core Schema" vs "Metadata Schema" choice) ## Changes ### Backend - **Deleted**: `mcp-metadata.controller.ts`, `mcp-metadata.service.ts` (merged into core) - **New**: `get-tool-catalog.tool.ts` — browsable, categorized tool discovery - **Fixed**: `DatabaseToolProvider.generateDescriptors` — removed guard that blocked API key access to CRUD tools - **Fixed**: `ToolContext` type + `ToolRegistryService` — `authContext` now flows through so API key CRUD execution works end-to-end - **Updated**: `McpProtocolService` — builds `ApiKeyWorkspaceAuthContext` for API key requests - **Updated**: All MCP tool descriptions with explicit step numbering ### Frontend - **Simplified**: `SettingsAIMCP.tsx` — removed schema selector dropdown, shows single MCP config ## Test plan - [x] All 19 MCP unit tests pass (controller + protocol service) - [x] Server and frontend lint clean - [x] Server and frontend typecheck pass - [x] Live-tested on localhost:3000 with API key: `get_tool_catalog` returns 236 tools including 196 DATABASE_CRUD tools - [x] Live-tested `execute_tool` with `find_companies` via API key — returns real data - [x] Tested MCP connection from Cursor IDE via project-level `.cursor/mcp.json` Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
da064d5e88 |
Support define is tool logic function (#17926)
- supports isTool and timeout settings in defineLogicFunction in apps and in setting tabs definition - compute for all toolInputSchema for logic funciton, in settings and in code steps <img width="991" height="802" alt="image" src="https://github.com/user-attachments/assets/05dc1221-cac9-45a3-87b0-3b13161446fd" /> |
||
|
|
84afbb4d2c |
feat: add draft email workflow action (#17793)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
21c51ec251 |
Improve AI agent chat, tool display, and workflow agent management (#17876)
## Summary - **Fix token renewal endpoint**: Use `/metadata` instead of `/graphql` for token renewal in agent chat, fixing auth issues - **Improve tool display**: Add `load_skills` support, show formatted tool names (underscores → spaces) with finish/loading states, display tool icons during loading, and support custom loading messages from tool input - **Refactor workflow agent management**: Replace direct `AgentRepository` access with `AgentService` for create/delete/find operations in workflow steps, improving encapsulation and consistency - **Simplify Apollo client usage**: Remove explicit Apollo client override in `useGetToolIndex`, add `AgentChatProvider` to `AppRouterProviders` - **Fix load-skill tool**: Change parameter type from `string` to `json` for proper schema parsing - **Update agent-chat-streaming**: Use `AgentService` for agent resolution and tool registration instead of direct repository queries ## Test plan - [ ] Verify AI agent chat works end-to-end (send message, receive response) - [ ] Verify tool steps display correctly with icons and proper messages during loading and after completion - [ ] Verify workflow AI agent step creation and deletion works correctly - [ ] Verify workflow version cloning preserves agent configuration - [ ] Verify token renewal works when tokens expire during agent chat Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
9e21e55db4 |
Prevent leak between /metadata and /graphql GQL schemas (#17845)
## Fix resolver schema leaking between `/metadata` and `/graphql` endpoints ### Summary - Patch `@nestjs/graphql` to support a `resolverSchemaScope` option that filters resolvers at both schema generation and runtime, preventing cross-endpoint leaking - Introduce `@CoreResolver()` and `@MetadataResolver()` decorators to explicitly scope each resolver to its endpoint - Move most resolvers (auth, billing, workspace, user, etc.) to the metadata schema where the frontend expects them; only workflow and timeline calendar/messaging resolvers remain on `/graphql` - Fix frontend `SSEQuerySubscribeEffect` to use the default (metadata) Apollo client instead of the core client ### Problem NestJS GraphQL's module-based resolver discovery traverses transitive imports, causing resolvers from `/metadata` modules to leak into the `/graphql` schema and vice versa. This made the schemas unpredictable and tightly coupled to module import order. ### Approach - Added `resolverSchemaScope` to `GqlModuleOptions` via a patch on `@nestjs/graphql`, filtering in both `filterResolvers()` (runtime binding) and `getAllCtors()` (schema generation) - Each resolver is explicitly decorated with `@CoreResolver()` or `@MetadataResolver()` - Organized decorator, constant, and type files under `graphql-config/` following project conventions Core GQL Schema: (see: no more fields!) <img width="827" height="894" alt="image" src="https://github.com/user-attachments/assets/668f3f0f-485e-43f0-92be-4345aeccacb6" /> Metadata GQL Schema (see no more getTimelineCalendarEventsFromCompany) <img width="827" height="894" alt="image" src="https://github.com/user-attachments/assets/443913db-e5fe-4161-b0e7-4a971cc80a71" /> |
||
|
|
3216b634a3 |
feat: improve AI chat - system prompt, tool output, context window display (#17769)
⚠️ **AI-generated PR — not ready for review** ⚠️ cc @FelixMalfait --- ## Changes ### System prompt improvements - Explicit skill-before-tools workflow to prevent the model from calling tools without loading the matching skill first - Data efficiency guidance (default small limits, use filters) - Pluralized `load_skill` → `load_skills` for consistency with `load_tools` ### Token usage reduction - Output serialization layer: strips null/undefined/empty values from tool results - Lowered default `find_*` limit from 100 → 10, max from 1000 → 100 ### System object tool generation - System objects (calendar events, messages, etc.) now generate AI tools - Only workflow-related and favorite-related objects are excluded ### Context window display fix - **Bug**: UI compared cumulative tokens (sum of all turns) against single-request context window → showed 100% after a few turns - **Fix**: Track `conversationSize` (last step's `inputTokens`) which represents the actual conversation history size sent to the model - New `conversationSize` column on thread entity with migration ### Workspace AI instructions - Support for custom workspace-level AI instructions --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
9162685b2e |
Reorganize logic function files (#17766)
reorganize according to <img width="1243" height="725" alt="Pasted Graphic" src="https://github.com/user-attachments/assets/ba65dd10-8eec-4b13-ad49-9726edd3b79c" /> Not working yet |
||
|
|
476bdf764c |
Refactor flat entity maps to be universal oriented (#17665)
# Introduction
In preparation of the workspace agnostic builder, we're migrating
`FlatEntityMaps` to be universal identifier oriented and based
As in the builder context there're won't be any ids at all
Please also note that the FlatEntity is a UniversalFlatEntity superset
From
```ts
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
export type FlatEntityMaps<T extends SyncableFlatEntity> = {
byId: Partial<Record<string, T>>;
idByUniversalIdentifier: Partial<Record<string, string>>;
universalIdentifiersByApplicationId: Partial<Record<string, string[]>>;
};
```
To
```ts
export type FlatEntityMaps<
T extends SyncableFlatEntity | UniversalSyncableFlatEntity,
> = {
byUniversalIdentifier: Partial<Record<string, T>>;
universalIdentifierById: Partial<Record<string, string>>;
universalIdentifiersByApplicationId: Partial<Record<string, string[]>>; // this might make more sense to be migrated to universalIdentifiersByApplicationUniversalIdentifier but it's the main topic of this PR
};
```
## Low level maps tools
Had to refactor find | create | delete | replace | find-many | get-sub
tools ( through mutations and or throw equivalent )
|
||
|
|
46d28509b9 |
Keep simplifying logic functions (#17595)
## Summary Refactors the `LogicFunctionService` API by consolidating v1 and v2 services: In metadata-module (presentation layer module) - **Renamed methods**: `deleteOneLogicFunction` → `destroyOne`, `updateOneLogicFunction` → `updateOne`, `createOneLogicFunction` → `createOne` - **Added duplicate methods**: `duplicateLogicFunction`, `createLogicFunctionFromExistingLogicFunctionById` - **Removed soft delete/restore** functionality - only hard delete (`destroyOne`) is supported In core-module (lower level module) - **Moved execution methods** to `LogicFunctionExecutorService` which is lower level: `executeOneLogicFunction`, `getAvailablePackages`, `getLogicFunctionSourceCode` |
||
|
|
6eebf6f23a |
Remove versions from logicFunction (#17540)
## Summary - Remove `latestVersion` and `publishedVersions` columns from `LogicFunction` entity - Remove version parameter from logic function execution, build, and source code retrieval - Update all related services, DTOs, utilities, and tests - Add database migration to drop the version columns |
||
|
|
fc908e9d87 |
Refactor WorkspaceAuthContext to use discriminated union types (#17491)
## Context The previous WorkspaceAuthContext was a single interface with many optional fields, making it unclear which fields are available in different authentication scenarios. This made the code harder to reason about and required runtime checks scattered throughout the codebase. ## Changes - Introduced a discriminated union type for WorkspaceAuthContext with four specific variants: -> UserWorkspaceAuthContext - for authenticated users -> ApiKeyWorkspaceAuthContext - for API key authentication -> ApplicationWorkspaceAuthContext - for application-based auth -> SystemWorkspaceAuthContext - for system/internal operations - Added type guard functions (isUserAuthContext, isApiKeyAuthContext, etc.) for safe type narrowing - Added builder utilities (buildUserAuthContext, buildApiKeyAuthContext, etc.) to construct each context variant with proper type safety - Refactored WorkspaceAuthContextMiddleware to use the new builders instead of constructing a loosely-typed object - Moved the type definition from twenty-orm/interfaces/ to core-modules/auth/types/ for better organization - Updated all consumers across query runners, tool providers, and modules to use the new type location ## Notes - I had to query User and WorkspaceMember in some parts of tool module that were expecting userWorkspaceId but not the rest of UserWorkspaceAuthContext (that should be required with the new proper type otherwise it would break a lot of logic and mostly permissions with the newly added RLS -> This is what we expect from UserWorkspaceAuthContext and how it's done in the "normal" path in HTTP middleware) - WorkspaceMember is in the cache already but ideally we should move User (And Workspace?) in the cache as well to avoid querying the DB after each request (this is also valid for HTTP middleware when we hydrate the Request object btw) |
||
|
|
da6f1bbef3 |
Rename serverlessFunction to logicFunction (#17494)
## Summary Rename "Serverless Function" to "Logic Function" across the codebase for clearer naming. ### Environment Variable Changes | Old | New | |-----|-----| | `SERVERLESS_TYPE` | `LOGIC_FUNCTION_TYPE` | | `SERVERLESS_LAMBDA_REGION` | `LOGIC_FUNCTION_LAMBDA_REGION` | | `SERVERLESS_LAMBDA_ROLE` | `LOGIC_FUNCTION_LAMBDA_ROLE` | | `SERVERLESS_LAMBDA_SUBHOSTING_URL` | `LOGIC_FUNCTION_LAMBDA_SUBHOSTING_URL` | | `SERVERLESS_LAMBDA_ACCESS_KEY_ID` | `LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID` | | `SERVERLESS_LAMBDA_SECRET_ACCESS_KEY` | `LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY` | ### Breaking Changes - Environment variables must be updated in production deployments - Database migration renames `serverlessFunction` → `logicFunction` tables |
||
|
|
942d2fef83 |
Remove sync-metadata and IS_WORKSPACE_CREATION_V2_ENABLED feature flag (#16997)
# Introduction Followup of https://github.com/twentyhq/twenty/pull/17001#pullrequestreview-3638508738 close https://github.com/twentyhq/core-team-issues/issues/1910 We've completely decom the `sync-metadata` in production. We're now then removing its implementation in favor of the v2. ## TODO: - [x] Remove sync-metadata implem and commands - [x] Remove workspace decorators - [x] Type each deprecated field to deprecated on their workspaceEntity - [x] Remove the `workspace-sync-metadata` folder entirely - [x] remove workspace migration - [x] workspace migration removal migration - [x] remove the `v2` references from workspace manager file names - [x] remove the `v2` references from workspace manager modules - [ ] Double check impact on translation file path updates ## Note - Removed the gate logic - Remains some service v2 naming, serverless needs to be migrated on v2 fully - Removed workspaceMigration service app health consumption, making it always returning up ( no more down ) cc @FelixMalfait ( quite obsolete health check now, will require complete refactor once we introduce inter app dependency etc ) |