Skip to content

Duplicate agent#1663

Open
sarah-inkeep wants to merge 21 commits intomainfrom
duplicate-agent
Open

Duplicate agent#1663
sarah-inkeep wants to merge 21 commits intomainfrom
duplicate-agent

Conversation

@sarah-inkeep
Copy link
Contributor

@sarah-inkeep sarah-inkeep commented Jan 31, 2026

sarah-inkeep and others added 12 commits January 30, 2026 15:18
- Create DuplicateAgentRequestSchema in schemas.ts
- Validate newAgentId as required string (2-256 chars, URL-safe)
- Validate newAgentName as optional string (1-255 chars)
- Response returns AgentWithinContextOfProjectSelectSchema
- Add comprehensive schema validation tests for boundary cases
- Tests cover valid IDs, invalid characters, length boundaries, reserved names

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implemented duplicateAgent() function in agents data access layer to copy an
agent and all its relationships while preserving data integrity.

Features:
- Copy agent record with new ID and name (defaults to '{originalName} (Copy)')
- Copy all sub-agents preserving original IDs
- Copy all agent-scoped function tools with new IDs
- Copy all agent-scoped context configs
- Copy all sub-agent relations (transfer/delegate)
- Copy all sub-agent tool relations with toolPolicies and headers
- Copy all sub-agent function tool relations with remapped function tool IDs
- Copy all sub-agent external agent relations
- Copy all sub-agent team agent relations
- Copy all sub-agent data component relations
- Copy all sub-agent artifact component relations
- Explicitly exclude triggers from duplication scope
- All operations preserve original timestamps

Tests:
- Comprehensive unit tests covering all entity types
- Tests for basic duplication with default and custom names
- Tests for error handling when source agent not found
- Tests for all relationship types
- Tests for comprehensive duplication with multiple relationships

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Added POST /{tenantId}/projects/{projectId}/agents/{agentId}/duplicate endpoint
- Validates newAgentId (required) and newAgentName (optional) via DuplicateAgentRequestSchema
- Returns 201 Created with full agent definition on success
- Returns 404 if original agent not found
- Returns 409 Conflict if newAgentId already exists
- Returns 400 Bad Request for invalid ID format
- Returns 403 Forbidden if user lacks project access (via requireProjectPermission middleware)
- Defaults newAgentName to '{originalName} (Copy)' if not provided
- Added comprehensive integration tests for all success and error scenarios
- Updated OpenAPI snapshot
- All tests, typecheck, and lint pass
- Added 'Duplicate agent' menu item to agent card dropdown
- Positioned between 'Edit' and 'Delete' menu items
- Added Copy icon from lucide-react
- Added isDuplicateOpen state for modal control
- Added placeholder for DuplicateAgentModal (to be implemented in US-005)
- Added unit tests for AgentItemMenu component
- All typecheck, lint, and format checks pass

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ents in agent duplication

**Bug:** When duplicating an agent with data/artifact components, the components
appeared duplicated in BOTH the original and copied agents due to missing agentId
filter in getFullAgentDefinitionInternal.

**Root Cause:** The query for data/artifact component relations only filtered by
tenantId and subAgentId, not agentId. Since sub-agents can have the same IDs
across different agents (preserved during duplication), this caused the query to
return relations from ALL agents with that sub-agent ID.

**Fix:**
- Added projectId and agentId filters to data component relation queries (line 475-477)
- Added projectId and agentId filters to artifact component relation queries (line 484-486)

**Testing:**
- Added regression test: "should not cause cross-contamination of data/artifact components"
- Verifies both agents show exactly 1 of each component (not duplicated)
- Verifies database has exactly 2 relations (1 per agent, not mixed)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…micity

**Critical Fix**: The duplicateAgent function performed 10+ database operations
without transaction wrapping, risking partial duplications on failure.

**Risk**: If any operation failed midway (network timeout, constraint violation),
the database would contain an orphaned partial agent with inconsistent state.

**Fix**:
- Wrapped entire duplicateAgent operation in db.transaction()
- All database operations now use transaction context (tx)
- On failure: automatic rollback, no orphaned records
- On success: all operations committed atomically

**Testing**:
- All 15 duplication tests pass
- Typecheck passes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…r performance

**Performance Issue**: Sequential for...of loops with individual awaits created
N+1 query pattern. Agent with 10 sub-agents + 5 tools + 12 relations = 27+
sequential database round-trips (40-1400ms total latency).

**Fix**: Convert all sequential inserts to batch inserts using Drizzle's
multi-value INSERT support.

**Before** (Sequential):
```typescript
for (const sub of subs) {
  await tx.insert(subAgents).values({...}); // 10 queries
}
```

**After** (Batched):
```typescript
await tx.insert(subAgents).values(
  subs.map(sub => ({...})) // 1 query
);
```

**Performance Improvement**:
- Before: 27+ sequential queries (~40-1400ms)
- After: 9 batched queries (~10-50ms)
- **Up to 28x faster** for large agents

**Changes**:
- Batch insert sub-agents (10 → 1 query)
- Batch insert function tools (5 → 1 query)
- Batch insert context configs (n → 1 query)
- Batch insert all relation types (12 → 6 queries)
- Batch insert data/artifact components (4 → 2 queries)

**Testing**:
- All 15 duplication tests pass
- Typecheck passes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
**Dead Code Removed**: subAgentIdMapping was created but never used.

**Why it existed**: Likely copied from functionToolIdMapping pattern, but
sub-agent IDs are intentionally preserved (not remapped) during duplication,
making the mapping unnecessary.

**Comparison**:
- functionToolIdMapping: NEEDED (function tools get new IDs)
- subAgentIdMapping: NOT NEEDED (sub-agents keep original IDs)

**Changes**:
- Removed unused Record<string, string> declaration
- Removed identity mapping assignment (sourceId -> sourceId)
- Added comment clarifying IDs are preserved

**Testing**:
- All 15 duplication tests pass
- Typecheck passes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The agent-item-menu and duplicate-agent-modal tests were placeholder
tests that didn't render components or test any actual behavior:
- agent-item-menu.test.tsx: Created empty div, asserted true === true
- duplicate-agent-modal.test.tsx: Only tested object properties and string interpolation

Real test coverage exists in:
- packages/agents-core/src/__tests__/data-access/duplicateAgent.test.ts (14 tests)
- agents-api/src/__tests__/manage/agent-duplicate.test.ts (8 tests)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@changeset-bot
Copy link

changeset-bot bot commented Jan 31, 2026

🦋 Changeset detected

Latest commit: 983118f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@inkeep/agents-core Patch
@inkeep/agents-manage-ui Patch
@inkeep/agents-api Patch
@inkeep/agents-cli Patch
@inkeep/agents-sdk Patch
@inkeep/ai-sdk-provider Patch
@inkeep/create-agents Patch
@inkeep/agents-manage-mcp Patch
@inkeep/agents-mcp Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Jan 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Feb 3, 2026 7:33pm
agents-docs Ready Ready Preview, Comment Feb 3, 2026 7:33pm
agents-manage-ui Ready Ready Preview, Comment Feb 3, 2026 7:33pm

Request Review

@claude
Copy link
Contributor

claude bot commented Jan 31, 2026

Claude finished @sarah-inkeep's task —— View job


Code Review Complete

This PR implements a comprehensive duplicate agent feature across all layers of the system. The implementation follows the repository's patterns well and includes proper testing, UI components, and API documentation. Here's my detailed review:

Strong Points

Architectural Design

  • Transaction-wrapped duplication: The duplicateAgent function properly wraps the entire operation in a database transaction, ensuring atomicity
  • Comprehensive relationship copying: Successfully duplicates all related entities (sub-agents, tools, relations, components, etc.)
  • Batch operations: Uses efficient batch inserts with Promise.all for performance optimization
  • ID mapping strategy: Smart handling of function tool ID remapping while preserving sub-agent IDs

Implementation Quality

  • Proper error handling: API route includes specific error codes (409 for conflicts) and appropriate HTTP status codes
  • Input validation: Strong Zod schemas with proper constraints (URL-safe IDs, length limits, reserved name prevention)
  • UI/UX: Clean modal implementation with auto-prefill ID functionality and proper loading states
  • Test coverage: Comprehensive unit and integration tests covering the core functionality

🔍 Areas for Consideration

Performance & Scalability

  1. Large agent duplication: The current implementation loads all relationships in memory simultaneously. For agents with hundreds of relationships, consider:

    // Consider pagination or streaming for very large agents
    const batchSize = 100;
    for (let i = 0; i < sourceSubAgents.length; i += batchSize) {
      const batch = sourceSubAgents.slice(i, i + batchSize);
      await tx.insert(subAgents).values(batch.map(...));
    }
  2. Deep copy vs shallow references: The implementation creates new IDs for most entities but preserves sub-agent IDs. This could lead to confusion - consider documenting this design decision or making ID generation more consistent.

Security & Authorization

  1. Permission inheritance: The API doesn't explicitly validate that the user has permission to create agents in the target project. While the existing auth middleware likely handles this, consider adding explicit checks for the duplication operation.

  2. Resource quotas: No validation of project limits - duplicating large agents could exceed tenant resource quotas.

Data Consistency

  1. Context config ID handling: Line 962 in packages/agents-core/src/data-access/manage/agents.ts:

    contextConfigId: sourceAgent.contextConfigId, // ⚠️ Potential issue

    This preserves the original context config ID, but context configs are agent-scoped. If the context config is duplicated separately, this could create a dangling reference.

  2. Circular relationships: No validation for potential circular sub-agent relationships that could be created through duplication.

Error Handling

  1. Partial failure recovery: If duplication fails partway through, the transaction rollback is good, but error messages could be more specific about which stage failed:
    try {
      // Add stage tracking for better error reporting
      logger.debug('Duplicating sub-agents...');
      // ... sub-agent duplication
      logger.debug('Duplicating function tools...');
      // ... function tool duplication
    } catch (error) {
      logger.error('Duplication failed during: ' + currentStage, error);
      throw error;
    }

API Design

  1. Response completeness: The API returns only the basic agent object. Consider returning a summary of what was duplicated:
    return c.json({ 
      data: duplicatedAgent,
      metadata: {
        duplicatedSubAgents: sourceSubAgents.length,
        duplicatedFunctionTools: sourceFunctionTools.length,
        // ... other counts
      }
    }, 201);

UI/UX Considerations

  1. Large agent duplication UX: The modal doesn't indicate duplication progress. For large agents, consider adding a progress indicator or moving to a background job pattern.

  2. Naming collision prevention: The auto-generated name pattern (Copy) could create confusion with multiple duplications. Consider (Copy 1), (Copy 2) pattern.

📝 Minor Code Quality Issues

  1. Type safety in fetchComponentRelationships: The function uses any types extensively (lines 215-232). Consider improving type safety:

    export const fetchComponentRelationships = <
      TRelation extends Record<string, unknown>,
      TComponent extends Record<string, unknown>
    >(
      // ... better typing
  2. Dead code in validation: Line 1129 in the duplicate function checks for null but the filter on line 1142 handles this - the null check might be redundant.

🎯 Architectural Considerations

Future Extensibility

  • Consider adding a duplicateOptions parameter to control what gets duplicated (e.g., exclude certain relationships)
  • The current design assumes all relationships should be duplicated - may need flexibility for different use cases

Audit & Observability

  • Consider adding audit logging for agent duplication events
  • The operation could benefit from OpenTelemetry tracing spans for debugging

Documentation Completeness

  • Missing public-facing documentation in /agents-docs/ (though this follows the pattern for internal features)
  • API documentation is auto-generated and complete

🏆 Overall Assessment

This is a well-implemented feature that demonstrates strong understanding of the codebase architecture. The implementation correctly follows the repository's patterns for:

  • ✅ Database operations with proper transaction handling
  • ✅ API route structure and error handling
  • ✅ UI component patterns and form validation
  • ✅ Comprehensive test coverage
  • ✅ Proper changeset creation

The performance optimizations using batch operations and the atomic transaction wrapper show thoughtful engineering. The identified considerations are primarily about edge cases and potential scalability improvements rather than fundamental issues.

Recommendation: Approve with the understanding that the identified performance and security considerations can be addressed in follow-up iterations as usage patterns emerge.


sarah-inkeep and others added 2 commits January 30, 2026 18:05
Add immediate loading toast feedback when duplicating an agent to improve UX.
The loading toast appears when the user clicks "Duplicate" and smoothly
transitions to either a success or error toast when the operation completes.

This provides better visibility than just the button text change, especially
for operations that may take a few seconds due to the transaction overhead.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Comment on lines +315 to +324
// PostgreSQL: code 23505
// Doltgres/MySQL: errno 1062 or message contains "duplicate primary key"
const isDuplicateKey =
errorCode === '23505' ||
topLevelMessage.includes('duplicate primary key') ||
topLevelMessage.includes('duplicate key') ||
topLevelMessage.includes('errno 1062') ||
causeMessage.includes('duplicate primary key') ||
causeMessage.includes('duplicate key') ||
causeMessage.includes('errno 1062');
Copy link
Collaborator

Choose a reason for hiding this comment

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

I want to do a refactor after you merge this functionality to better handle DB specific error code catching.

{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agent","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agent/{agentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agent/{agentId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agent/{agentId}","method":"put"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/full","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/agent/{subAgentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/agent/{subAgentId}/component/{artifactComponentId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/agent/{subAgentId}/component/{artifactComponentId}/exists","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/component/{artifactComponentId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/agent/{subAgentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/agent/{subAgentId}/component/{dataComponentId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/agent/{subAgentId}/component/{dataComponentId}/exists","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/component/{dataComponentId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/related","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{id}","method":"put"}]} showTitle={true} /> No newline at end of file
<APIPage document={"index"} webhooks={[]} operations={[{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agent","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agent/{agentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agent/{agentId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agent/{agentId}","method":"put"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/duplicate","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/full","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/agent/{subAgentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/agent/{subAgentId}/component/{artifactComponentId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/agent/{subAgentId}/component/{artifactComponentId}/exists","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-artifact-components/component/{artifactComponentId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components","method":"post"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/agent/{subAgentId}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/agent/{subAgentId}/component/{dataComponentId}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/agent/{subAgentId}/component/{dataComponentId}/exists","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agent-data-components/component/{dataComponentId}/agents","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{agentId}/sub-agents/{subAgentId}/related","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{id}","method":"get"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{id}","method":"delete"},{"path":"/manage/tenants/{tenantId}/projects/{projectId}/agents/{id}","method":"put"}]} showTitle={true} /> No newline at end of file
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just to confirm, this change was generated from the script and fumadocs package, yes?

} from '../ui/dialog';
import { Form } from '../ui/form';

const duplicateAgentSchema = z.object({
Copy link
Collaborator

Choose a reason for hiding this comment

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

This schema doesn't match the API. The API has a max character restriction. Can we derive the duplicateAgentSchema from the schema exported from agents-core? Then we will be able to have our frontend and backend schema validation automatically in sync.

const form = useForm<DuplicateAgentFormData>({
resolver: zodResolver(duplicateAgentSchema),
defaultValues: {
newAgentName: `${agentName} (Copy)`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like this might be duplicate default value for newAgentName? other value is defined in manage/routes/agent.ts (https://github.com/inkeep/agents/pull/1663/changes#diff-dd956509456b777e1f3f68474c6ce67478761c28396fdabb7e576c33ded6e7a4R299) and described in a third location here: https://github.com/inkeep/agents/pull/1663/changes#diff-a6b7ad468243ec559f66f3a1cb18b172f224383d7a2f31a15a6c3657f20103d3R2204-R2212 agents-core/validation/schemas.ts

.describe(
'Name for the duplicated agent. Defaults to "{originalName} (Copy)" if not provided.'
),
})
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we make newAgentName schema inherit from the original agentName schema so that they remain in sync?

id: newAgentId,
tenantId,
projectId,
name: newAgentName || `${sourceAgent.name} (Copy)`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

default name is defined yet again at the data access layer. I think we need to reduce default name definitions to one location.

);
}

const sourceFunctionTools = await tx
Copy link
Collaborator

@amikofalvy amikofalvy Feb 2, 2026

Choose a reason for hiding this comment

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

optimization that should be done within this PR. Parallel data fetches for all resources, otherwise this operation will take longer than it needs to.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Additionally, parallel inserts if at all possible, there will need to be some sequencing to make sure dependencies exist before inserting some types of resources.

Additionally, think forward into the future, how will maintaining this duplication functionality have to change if we add other types of resources. I'm worried that we will add a new feature like "Agent Skills" and forget to include it in the duplication functionality. The challenge here is that all duplicated resources as explicitly defined instead of dynamically discovered.

Copy link
Collaborator

@amikofalvy amikofalvy left a comment

Choose a reason for hiding this comment

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

need to look at schema re-use, parallel db executions when possible, and making it easier to maintain this functionality when we add new agent resource types in the near and far future.

…nt cross-contamination

Function tools now get their own copy of the underlying function record,
so modifying code or input schema in a duplicated agent no longer affects the original.
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.

2 participants