Skip to content

Revamp custom agents: DB-backed defaults, unified chat, and delegation#506

Merged
larryro merged 19 commits into
mainfrom
feature/custom-agents-revamp
Feb 21, 2026
Merged

Revamp custom agents: DB-backed defaults, unified chat, and delegation#506
larryro merged 19 commits into
mainfrom
feature/custom-agents-revamp

Conversation

@larryro
Copy link
Copy Markdown
Collaborator

@larryro larryro commented Feb 20, 2026

Summary

  • Replace hardcoded built-in agents with DB-backed system defaults — agents are now seeded as custom_agents records with a systemDefault flag, eliminating per-type boilerplate (removed ~2,600 lines of duplicated agent mutations, internal actions, and config)
  • Unified chat system — all agent types now go through a single unified_chat path instead of separate per-agent chat/mutation/action files
  • Agent delegation — new delegation tab and tool allowing agents to hand off tasks to other active custom agents at runtime
  • Manual save mode for useAutoSave — instructions tab now saves on blur/value change instead of debounced auto-save, with explicit save() API and tests

Test plan

  • Verify system default agents are seeded correctly on fresh environment
  • Test chat with each system default agent type (chat, web, crm, document, integration, workflow)
  • Test creating and chatting with custom agents
  • Test agent delegation: agent A delegates to agent B during conversation
  • Verify inactive agents are filtered from delegation list
  • Test instructions tab: changes save on blur, model/toggle changes save immediately
  • Run useAutoSave hook tests

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added agent delegation capability allowing agents to delegate tasks to other agents
    • Introduced agent visibility control to show/hide agents in chat interface
  • Improvements

    • Unified agent system architecture for consistent handling across all agent types
    • Enhanced chat availability messaging when agents are unavailable
  • Documentation

    • Added localization strings for delegation management and agent visibility settings

…elegation

Convert 6 hardcoded built-in agents (Assistant, Search, Sales, Docs, Connect, Automate) into database records
seeded per-organization, giving users full control to edit, enable/disable, and reconfigure all agents.

Introduce a partner agent system where any agent can designate other agents as delegation partners. At runtime,
dynamic `partner_<name>` tools are generated via `createPartnerDelegationTool()`, replacing the old hardcoded
sub-agent tools (crm_assistant, document_assistant, integration_assistant, web_assistant, workflow_assistant).

Key changes:
- Schema: add partnerAgentIds, maxSteps, timeoutMs, outputReserve, isSystemDefault, systemAgentSlug,
  roleRestriction fields to customAgents table
- Seeding: system_defaults.ts templates + seed_system_defaults.ts for per-org initialization
- Partner delegation: generic tool factory with context validation, budget checks, role access, sub-thread reuse
- Unified chat: single chatWithAgent mutation replaces 3 separate entry points
- Frontend: unified agent selector and mutation hooks, removed builtin/custom split
- Cleanup: deleted 25 files of old builtin agent infrastructure (-2591 lines)
…gation UI

Remove the isActive soft-delete pattern in favor of hard deletes and status-based
queries. Refactor the chat agent from a pure router into a general-purpose assistant
with direct knowledge base and web search access. Add delegation configuration page
for managing agent-to-agent task routing.
…ity toggle

Rename the "partner agent" concept to "delegation" throughout the codebase
for clearer semantics — partnerAgentIds becomes delegateAgentIds with a
backward-compatible fallback on read. Add a visibleInChat field so agents
like the document assistant can be hidden from the chat selector while still
being available through delegation. Disable chat input when no agents are
published. Improve PPTX tool messaging to direct users to the Knowledge
Base page with a direct link.
…for instructions

- Add 'manual' mode to useAutoSave that tracks dirty state but only
  persists when save() is called explicitly or on unmount
- Switch instructions tab to manual save, triggering on blur and
  value change instead of debounced auto-save
- Filter inactive agents from delegation list
- Add tests for useAutoSave hook
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Feb 20, 2026

Greptile Summary

This PR successfully consolidates agent architecture by replacing hardcoded built-in agents with DB-backed system defaults, eliminating ~2,600 lines of duplicated code.

Key Changes:

  • Unified chat system routes all agents through single chatWithAgent mutation instead of three separate endpoints
  • System default agents now seeded as customAgents records with systemAgentSlug for stable cross-org references
  • New delegation system replaces hardcoded sub-agent tools with dynamic tool generation at runtime
  • Schema changes deprecate isActive field, add delegateAgentIds, visibleInChat, and system default metadata
  • Frontend simplified to fetch all agents from DB instead of mixing builtin/custom types
  • Manual save mode added to useAutoSave for blur-triggered saves on instructions tab

Breaking Changes:

  • Agent deletion now permanently removes all versions instead of soft-delete (mark inactive)
  • Removed index by_org_active_status — existing queries updated to use by_organization or by_org_status

Migration Considerations:

  • Existing orgs need ensureSystemDefaults called to seed system agents
  • Backward compatibility maintained via partnerAgentIds fallback to delegateAgentIds

Confidence Score: 4/5

  • Safe to merge with thorough testing of system defaults seeding and delegation flows
  • Score reflects well-architected refactoring with good backward compatibility handling, but significant schema changes and deletion behavior change require careful testing of migration path and user workflows
  • Pay close attention to schema.ts (index removal), mutations.ts (hard delete behavior), and seed_system_defaults.ts (seeding logic for existing orgs)

Important Files Changed

Filename Overview
services/platform/convex/custom_agents/unified_chat.ts New unified chat entry point consolidating three separate chat mutations into one
services/platform/convex/custom_agents/seed_system_defaults.ts Idempotent seeding logic for system defaults with two-pass delegate resolution
services/platform/convex/agent_tools/delegation/create_delegation_tool.ts Dynamic delegation tool factory replacing hardcoded sub-agent tools
services/platform/convex/custom_agents/schema.ts Schema changes deprecating isActive, adding delegateAgentIds and system default fields; removes index by_org_active_status
services/platform/convex/custom_agents/mutations.ts Mutation updates to support delegation and remove isActive checks; deletion now removes all versions
services/platform/convex/custom_agents/queries.ts Query updates to use new indexes and filter by visibleInChat instead of isActive
services/platform/convex/lib/agent_chat/internal_actions.ts Enhanced with dynamic delegation tool loading and instruction appending at runtime
services/platform/app/features/chat/components/agent-selector.tsx Refactored to use DB-backed system defaults instead of hardcoded builtin types

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User sends message] --> B{Agent selected?}
    B -->|No| C[Resolve default chat agent]
    B -->|Yes| D[Use selected agentId]
    C --> E[Query by systemAgentSlug='chat']
    D --> F[Load active version by rootVersionId]
    E --> F
    F --> G[Check team access]
    G --> H[Build agent config]
    H --> I{Has delegateAgentIds?}
    I -->|Yes| J[Load delegate agents from DB]
    I -->|No| K[Create agent with tools]
    J --> L[Generate delegation tools dynamically]
    L --> M[Append delegation instructions]
    M --> K
    K --> N[Execute unified chat via startAgentChat]
    N --> O{Agent uses delegation?}
    O -->|Yes| P[Create sub-thread by delegate rootVersionId]
    O -->|No| Q[Complete response]
    P --> R[Run delegate via runAgentGeneration]
    R --> Q
Loading

Last reviewed commit: f009d28

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

81 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

// @deprecated — kept for existing documents, no longer written or read
partnerAgentIds: v.optional(v.array(v.id('customAgents'))),
maxSteps: v.optional(v.number()),
timeoutMs: v.optional(v.number()),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed index by_org_active_status but isActive field still marked as optional instead of fully deprecated

Suggested change
timeoutMs: v.optional(v.number()),
// @deprecated — kept for existing documents, no longer written or read
isActive: v.optional(v.boolean()),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isActive field already has the deprecation comment on line 53: // @deprecated — kept for existing documents, no longer written or read. The suggested code is identical to what's already in the file. In Convex, schema fields with existing document data cannot be fully removed — keeping it as v.optional(v.boolean()) with the deprecation comment is the correct approach for Convex schemas.

If you agree with the changes and no further modifications are needed, please resolve this conversation.

Comment on lines +86 to +91
if (existing?.rootVersionId) {
slugToId.set(template.systemAgentSlug, existing.rootVersionId);
} else {
const agentId = await insertSystemAgent(ctx, organizationId, template);
slugToId.set(template.systemAgentSlug, agentId);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if an existing agent has no rootVersionId, it won't be included in the map but the code continues silently

check whether this is expected behavior for existing agents without rootVersionId

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. The original existing?.rootVersionId condition would fall through to insertSystemAgent when an existing agent lacked rootVersionId, creating a duplicate. Fixed by splitting the check: if the agent exists but has no rootVersionId, we backfill it (patch rootVersionId to existing._id) and use it in the map instead of inserting a duplicate. Typecheck passes.

If you agree with the changes and no further modifications are needed, please resolve this conversation.

teamId: source.teamId,
sharedWithTeamIds: source.sharedWithTeamIds,
createdBy: source.createdBy,
delegateAgentIds: source.delegateAgentIds ?? source.partnerAgentIds,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backward compatibility: falls back to partnerAgentIds when delegateAgentIds is undefined

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This backward compatibility fallback is intentional and correct. The partnerAgentIds field is deprecated in the schema (marked with @deprecated comment) but existing documents may still have it set. The ?? fallback ensures those records are handled properly when copying version fields during duplication or draft creation. The same pattern is used consistently in config.ts line 59. No change needed.

If you agree with the changes and no further modifications are needed, please resolve this conversation.

Comment on lines +319 to +323
const allVersions = ctx.db
.query('customAgents')
.withIndex('by_root', (q) => q.eq('rootVersionId', args.customAgentId));

for await (const version of allVersions) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deletion now removes ALL versions of an agent (draft, active, archived) instead of just marking as inactive

verify this behavior change won't cause unintended data loss for users who expect soft deletion

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hard-delete behavior is intentional — isActive is deprecated and the new versioning system uses status as the canonical lifecycle state. However, this flagged a real issue: associated webhooks in customAgentWebhooks were not being cleaned up, leaving dangling references. Fixed by deleting associated webhooks (via the by_agent index) before removing agent versions. The delegateAgentIds references in other agents are handled gracefully since loadDelegateAgents already tolerates missing agents.

If you agree with the changes and no further modifications are needed, please resolve this conversation.

Comment on lines +45 to +49
for await (const agent of systemChat) {
if (agent.status === 'active' && agent.rootVersionId) {
return agent.rootVersionId;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

query returns only the first active agent with slug chat, but doesn't validate there's exactly one

if multiple active chat agents exist for an org (data inconsistency), this silently picks the first one

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining this suggestion. The "pick first active" pattern is intentional and used consistently across the codebase (also in seed_system_defaults.ts and human_input/mutations.ts). Multiple documents with the same systemAgentSlug are expected — they are versions of the same agent, all sharing the same rootVersionId. Since line 47 returns agent.rootVersionId, any active version resolves to the same root ID regardless of which one is picked first. The seeding function is idempotent and prevents duplicate root agents per slug per org, so the data inconsistency scenario described cannot occur through normal code paths.

If you agree with the changes and no further modifications are needed, please resolve this conversation.

);

const results: Doc<'customAgents'>[] = [];
for await (const agent of agents) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added filter for visibleInChat to exclude agents marked as non-visible (e.g., document assistant used only via delegation)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a descriptive comment summarizing the code's behavior, not a change request. The visibleInChat === false filter on line 74 is correctly placed: it only applies in the filterPublished === true branch, which is used by the chat page to select agents. The management UI calls listCustomAgents without filterPublished, so all agents (including non-visible ones) correctly appear there for configuration. The implementation is sound.

If you agree with the changes and no further modifications are needed, please resolve this conversation.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

This PR consolidates the agent system from separate builtin and custom types into a unified custom agent model. Hard-coded agent definitions (chat, web, crm, document, integration, workflow) are removed and replaced with database-seeded system default agents. The PR eliminates separate chat mutations (chatWithAgent, chatWithBuiltinAgent, chatWithCustomAgent) in favor of a single unified chatWithAgent. Sub-agent tools (workflow_assistant, document_assistant, integration_assistant, crm_assistant) are removed and replaced with a delegation model where agents can delegate tasks to other agents using a delegate_ prefix naming convention. System agents are auto-seeded on first use with pre-configured instructions, tools, and delegate relationships. UI components and type systems are updated to reflect this unified architecture.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • PR #422: Refactors agent selector, chat input/interface, and custom agent hooks/mutations with overlapping code changes to the same UI and API components.
  • PR #430: Modifies custom-agent-table.tsx to add visibility toggles for agents, touching the same custom agent UI surfaces modified in this PR.
  • PR #185: Introduces overlapping changes to sub-agent infrastructure, context-management routing, and delegation logic within the agent tooling system.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Revamp custom agents: DB-backed defaults, unified chat, and delegation' accurately summarizes the main changes: migrating from hardcoded built-in agents to database-backed system defaults, implementing a unified chat flow, and adding delegation functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/custom-agents-revamp

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
services/platform/convex/agent_tools/files/pptx_tool.ts (1)

239-250: ⚠️ Potential issue | 🟡 Minor

Harden SITE_URL handling to avoid malformed links.

If SITE_URL is empty or ends with a trailing slash, the message can embed a relative or double-slash URL. Suggest trimming and falling back to a path-only URL when needed.

🔧 Proposed fix
-          const siteUrl = process.env.SITE_URL || '';
-          const knowledgeUrl = `${siteUrl}/dashboard/${organizationId}/knowledge/documents`;
+          const siteUrl = (process.env.SITE_URL ?? '').replace(/\/$/, '');
+          const knowledgePath = `/dashboard/${organizationId}/knowledge/documents`;
+          const knowledgeUrl = siteUrl ? `${siteUrl}${knowledgePath}` : knowledgePath;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agent_tools/files/pptx_tool.ts` around lines 239 -
250, The code builds knowledgeUrl from process.env.SITE_URL into siteUrl and may
produce malformed URLs if SITE_URL is empty or has a trailing slash; update the
logic that computes siteUrl/knowledgeUrl (the variables siteUrl and knowledgeUrl
and any code that uses organizationId) to normalize SITE_URL by trimming
whitespace and any trailing slash and falling back to a path-only value (e.g.,
'/dashboard/<org>/knowledge/documents') when SITE_URL is empty; ensure
knowledgeUrl is constructed by concatenating a single slash and organizationId
(or by using a URL-join utility) so the returned message never contains double
slashes or a relative URL.
services/platform/app/features/chat/components/chat-input.tsx (1)

135-140: ⚠️ Potential issue | 🟡 Minor

Disable drag‑and‑drop uploads when the input is disabled.

FileUpload.DropZone still accepts dropped files even when disabled/isLoading is true, so users can upload attachments they cannot send (e.g., no agents available). Consider guarding onFilesSelected to no‑op in those states.

Proposed fix
+  const handleFilesSelected = (files: File[]) => {
+    if (disabled || isLoading) return;
+    void uploadFiles(files);
+  };
+
   return (
     <div {...restProps} className={cn('bg-background', restProps.className)}>
       <FileUpload.DropZone
         className="relative flex h-full min-h-0 flex-1 flex-col"
-        onFilesSelected={uploadFiles}
+        onFilesSelected={handleFilesSelected}
         clickable={false}
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/chat/components/chat-input.tsx` around lines
135 - 140, FileUpload.DropZone currently invokes uploadFiles even when the input
is disabled or isLoading; change its onFilesSelected prop to a guarded no-op
when disabled || isLoading (e.g., pass undefined or a noop) so dropped files are
ignored in those states. Locate the FileUpload.DropZone component in
chat-input.tsx and replace onFilesSelected={uploadFiles} with a conditional
handler that returns immediately when disabled or isLoading, leaving clickable
behavior unchanged.
services/platform/convex/custom_agents/mutations.ts (2)

282-299: ⚠️ Potential issue | 🟠 Major

Add organization membership validation in new mutations.

Both updateCustomAgentVisibility and deleteCustomAgent authenticate but don’t verify org membership, which can allow cross‑org changes for teamless agents. Insert the standard org‑membership check before hasTeamAccess.

Based on learnings: Enforce the established authorization flow in mutation handlers: authenticate → getOrganizationMember → perform operation, and do not introduce authorizeRls() into mutation handlers.

Also applies to: 303-317

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/custom_agents/mutations.ts` around lines 282 - 299,
The handlers updateCustomAgentVisibility and deleteCustomAgent authenticate
users but skip org-membership checks; add a call to getOrganizationMember(ctx,
authUser) immediately after obtaining authUser and before calling hasTeamAccess
(and throw if it returns falsy) to enforce organization membership for teamless
agents; keep the existing hasTeamAccess check and do not add authorizeRls() to
these mutation handlers.

352-359: ⚠️ Potential issue | 🟠 Major

Prevent duplicating system‑default flags on copies.

duplicateCustomAgent copies isSystemDefault/systemAgentSlug via copyVersionFields, which can create multiple “system default” agents and collide on slugs if a system default is duplicated. Clear those fields (or block duplication) on copies.

🔧 Suggested fix
     const newAgentId = await ctx.db.insert('customAgents', {
       ...copyVersionFields(source),
+      isSystemDefault: false,
+      systemAgentSlug: undefined,
       name: newName,
       displayName: newDisplayName,
       createdBy: String(authUser._id),
       versionNumber: 1,
       status: 'draft',
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/custom_agents/mutations.ts` around lines 352 - 359,
duplicateCustomAgent currently copies system-default flags through
copyVersionFields, which can create multiple system defaults and slug
collisions; update the insert in duplicateCustomAgent (the code that calls
ctx.db.insert('customAgents', { ...copyVersionFields(source), name: newName,
displayName: newDisplayName, ... })) to explicitly clear or override the
system-default fields (e.g., set isSystemDefault to false and systemAgentSlug to
null/undefined) when creating the new record so copies are never marked as
system defaults and cannot reuse the system slug.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/platform/app/features/chat/components/agent-selector.tsx`:
- Around line 80-95: The sort repeatedly calls allAgents.find inside system.sort
(see the aAgent/bAgent lookups), which is inefficient; before sorting, build a
lookup map (e.g., agentByKey) keyed by (agent.rootVersionId ?? agent._id) and
also account for the default-chat mapping (systemAgentSlug === 'chat') so the
default chat option can be resolved from the map, then replace the
allAgents.find calls in the system.sort comparator with O(1) lookups from
agentByKey and compute aOrder/bOrder from SYSTEM_SLUG_ORDER with the same 99
fallback.

In `@services/platform/app/features/chat/components/thinking-animation.tsx`:
- Around line 67-74: The delegate display logic using
toolName.startsWith('delegate_') should handle snake_case and empty slugs: when
extracting the slug from toolName (the slice after 'delegate_'), normalize it by
replacing underscores and dashes with spaces (or split on /[-_]/) before
capitalizing each word to build agentDisplayName, and if the resulting slug is
empty or only whitespace fall back to a safe default (e.g., the original
toolName or a literal like "Delegate") so displayText is never blank; update the
block that computes agentDisplayName to perform these checks and normalization.

In
`@services/platform/app/features/custom-agents/hooks/__tests__/use-auto-save.test.ts`:
- Around line 241-254: Add a unit test to cover the skip-save behavior when
override data equals the last saved value: render useAutoSave (useAutoSave) with
initial data and onSave, mutate to a new value and call result.current.save() to
create lastSavedRef, then call result.current.save(override) with an override
object equal to that last saved value and assert onSave was not called again
(expect onSave call count unchanged) and status remains appropriate; reference
the save method, lastSavedRef behavior, and onSave mock in your new test.
- Around line 99-123: Rename the test description string in the failing it(...)
block from "does not save when data reverts to the last saved value" to a
clearer name like "saves when reverting to a different value, skips duplicate
saves" to reflect both behaviors demonstrated (save when reverting to a
different lastSavedRef and no save for duplicate value), keeping the test body
and assertions for useAutoSave and onSave unchanged.

In
`@services/platform/app/routes/dashboard/`$id/custom-agents/$agentId/delegation.tsx:
- Around line 72-96: toggleAgent currently reads agent.delegateAgentIds and
selectedSet at call time which can be stale during rapid toggles; change to use
an atomic/functional update and optimistic mutation to avoid races: compute the
new delegate list from the latest state (use a ref or a state updater that
receives previous value, or use react-query's onMutate to apply an optimistic
update) and ensure subsequent toggles operate against that updated value (or
disable UI while updateAgent.mutateAsync is in-flight). Update references in
toggleAgent, updateAgent, saveWithStatus, selectedSet and agent.delegateAgentIds
to use the chosen functional/optimistic pattern so each toggle bases its next
array on the current/previous value rather than stale reads.

In
`@services/platform/app/routes/dashboard/`$id/custom-agents/$agentId/instructions.tsx:
- Around line 139-143: The onValueChange handlers call form.setValue(...) then
immediately call save(form.getValues()), which risks missing the new value if
setValue timing changes; instead compute the updated payload locally and pass it
to save (e.g., let updated = { ...form.getValues(), modelPreset: val } and call
save(updated)) and still call form.setValue(...) to keep the form state in sync;
apply the same change to the other identical handler that sets a form value then
calls save (the other onValueChange around the model setting at the later
block).

In `@services/platform/convex/custom_agents/internal_queries.ts`:
- Around line 15-29: The validation for args.rootVersionIds is incorrect:
replace v.array(v.string()) with v.array(v.id('customAgents')) so the handler's
args.rootVersionIds are Convex document IDs and you can remove the
toId<'customAgents'>(rootId) cast when querying the customAgents index; keep
organizationId as v.string() since it's an external auth identifier. Ensure the
args block in the handler uses v.array(v.id('customAgents')) and update any
usage of toId in the loop (e.g., where you build the by_root_status query on
customAgents) to use rootId directly.

In `@services/platform/convex/custom_agents/mutations.ts`:
- Around line 311-323: The deletion currently queries versions by
eq('rootVersionId', args.customAgentId) which fails if callers passed a non-root
version; instead derive the true root id from the fetched rootAgent (e.g., use
rootAgent.rootVersionId if present, otherwise args.customAgentId) and use that
normalized root id when building the allVersions cursor (the variable
allVersions and the withIndex('by_root') / eq('rootVersionId', ...) call) so all
child versions of the root are targeted for deletion.

In `@services/platform/convex/custom_agents/seed_system_defaults.ts`:
- Around line 86-91: The current logic inserts a new agent when an existing
record has no rootVersionId, causing duplicates; update the branch around
slugToId.set so that if existing is truthy but existing.rootVersionId is falsy
you set slugToId.set(template.systemAgentSlug, existing._id) as a fallback
instead of calling insertSystemAgent, and only call insertSystemAgent when
existing is completely absent; reference the symbols existing, rootVersionId,
existing._id, slugToId, template.systemAgentSlug, and insertSystemAgent when
applying the change.

In `@services/platform/convex/custom_agents/unified_chat.ts`:
- Around line 79-119: After authenticating via authComponent.getAuthUser and
before resolving or checking the agent (resolveAgentId, activeVersionQuery,
hasTeamAccess), validate the caller is a member of args.organizationId by
calling the standard getOrganizationMember(ctx, authUser._id,
args.organizationId) pattern and rejecting if not a member; then continue with
loading the activeVersion and team-access checks (use getUserTeamIds and
hasTeamAccess as before). Do not introduce authorizeRls() into this mutation
handler—follow authenticate → getOrganizationMember → perform operation flow.
- Around line 143-158: Before calling startAgentChat, fetch the thread by
args.threadId (e.g., using the same data-access used elsewhere in
unified_chat.ts), verify its organizationId equals args.organizationId, and
throw or return an authorization error if they differ; alternatively, add the
same check at the top of startAgentChat to ensure every caller is protected.
Make the check happen immediately after loading the conversation/thread and
before any inserts/patches or scheduling, referencing startAgentChat,
args.threadId and args.organizationId so the call is gated by the organizationId
equality check.

---

Outside diff comments:
In `@services/platform/app/features/chat/components/chat-input.tsx`:
- Around line 135-140: FileUpload.DropZone currently invokes uploadFiles even
when the input is disabled or isLoading; change its onFilesSelected prop to a
guarded no-op when disabled || isLoading (e.g., pass undefined or a noop) so
dropped files are ignored in those states. Locate the FileUpload.DropZone
component in chat-input.tsx and replace onFilesSelected={uploadFiles} with a
conditional handler that returns immediately when disabled or isLoading, leaving
clickable behavior unchanged.

In `@services/platform/convex/agent_tools/files/pptx_tool.ts`:
- Around line 239-250: The code builds knowledgeUrl from process.env.SITE_URL
into siteUrl and may produce malformed URLs if SITE_URL is empty or has a
trailing slash; update the logic that computes siteUrl/knowledgeUrl (the
variables siteUrl and knowledgeUrl and any code that uses organizationId) to
normalize SITE_URL by trimming whitespace and any trailing slash and falling
back to a path-only value (e.g., '/dashboard/<org>/knowledge/documents') when
SITE_URL is empty; ensure knowledgeUrl is constructed by concatenating a single
slash and organizationId (or by using a URL-join utility) so the returned
message never contains double slashes or a relative URL.

In `@services/platform/convex/custom_agents/mutations.ts`:
- Around line 282-299: The handlers updateCustomAgentVisibility and
deleteCustomAgent authenticate users but skip org-membership checks; add a call
to getOrganizationMember(ctx, authUser) immediately after obtaining authUser and
before calling hasTeamAccess (and throw if it returns falsy) to enforce
organization membership for teamless agents; keep the existing hasTeamAccess
check and do not add authorizeRls() to these mutation handlers.
- Around line 352-359: duplicateCustomAgent currently copies system-default
flags through copyVersionFields, which can create multiple system defaults and
slug collisions; update the insert in duplicateCustomAgent (the code that calls
ctx.db.insert('customAgents', { ...copyVersionFields(source), name: newName,
displayName: newDisplayName, ... })) to explicitly clear or override the
system-default fields (e.g., set isSystemDefault to false and systemAgentSlug to
null/undefined) when creating the new record so copies are never marked as
system defaults and cannot reuse the system slug.

Comment thread services/platform/app/features/chat/components/agent-selector.tsx
Comment thread services/platform/convex/custom_agents/internal_queries.ts
Comment thread services/platform/convex/custom_agents/mutations.ts
Comment thread services/platform/convex/custom_agents/seed_system_defaults.ts Outdated
Comment thread services/platform/convex/custom_agents/unified_chat.ts
Comment thread services/platform/convex/custom_agents/unified_chat.ts
…dge and web search

Replace the boolean knowledgeEnabled toggle with a four-way retrieval mode
for both knowledge base and web search. In "context" or "both" modes, RAG
and web results are automatically injected into the agent's structured
context before generation, rather than relying solely on tool calls.

- Add retrievalModeValidator and knowledgeMode/webSearchMode schema fields
- Replace syncRagSearchTool with syncRetrievalModes in mutations
- Add context injection paths in generate_response for knowledge and web
- New query_web_context helper for crawled page retrieval
- Update knowledge UI from toggle to radio group, add web search mode on tools tab
- Backward-compatible: derives mode from legacy fields for existing agents
Allow users to enable/disable structured response markers (conclusion,
key points, details, etc.) per agent via the instructions tab. Defaults
to enabled for backward compatibility.
… agent instructions

Remove the beforeContextHook and integrations/system-info context injection
pipeline in favor of template variables (e.g. {{organization.name}},
{{current_time}}) resolved at runtime within system instructions. This
simplifies context management and gives agent creators explicit control
over what dynamic data appears in their prompts.

- Add resolve_template_variables module with tests
- Remove beforeContextHook, integrationsInfo, contextFeatures, and system info injection
- Make hasIntegrations optional in schema for backward compatibility
- Add template variables reference UI to instructions tab
- Replace `as ModelPreset` type casts with Zod parsing
…ead of listing all

Prevents new custom agent fields from being silently dropped when
creating versions — copyVersionFields now spreads the source document
and deletes the version-meta keys rather than enumerating every
content field.
@larryro larryro merged commit 3cd35f5 into main Feb 21, 2026
17 checks passed
@larryro larryro deleted the feature/custom-agents-revamp branch February 21, 2026 05:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant