Conversation
- 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 detectedLatest commit: 983118f The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @sarah-inkeep's task —— View job Code Review CompleteThis 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 PointsArchitectural Design
Implementation Quality
🔍 Areas for ConsiderationPerformance & Scalability
Security & Authorization
Data Consistency
Error Handling
API Design
UI/UX Considerations
📝 Minor Code Quality Issues
🎯 Architectural ConsiderationsFuture Extensibility
Audit & Observability
Documentation Completeness
🏆 Overall AssessmentThis is a well-implemented feature that demonstrates strong understanding of the codebase architecture. The implementation correctly follows the repository's patterns for:
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. |
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>
| // 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'); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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)`, |
There was a problem hiding this comment.
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.' | ||
| ), | ||
| }) |
There was a problem hiding this comment.
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)`, |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
optimization that should be done within this PR. Parallel data fetches for all resources, otherwise this operation will take longer than it needs to.
There was a problem hiding this comment.
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.
amikofalvy
left a comment
There was a problem hiding this comment.
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.
Allow users to duplicate an agent in the same project as the original agent. https://linear.app/inkeep/issue/PRD-5932/allow-users-to-duplicate-and-modify-existing-agents-in-ui
https://www.loom.com/share/ae08b9b6d8d14af79045464aebc0d208